diff --git a/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx b/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx index 5954d48cd8..499e146f67 100644 --- a/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx +++ b/apps/tangle-cloud/src/pages/instances/Instances/UpdateBlueprintModel/ServiceRequestDetailModal.tsx @@ -16,6 +16,8 @@ import { type ServiceRequest } from '@tangle-network/tangle-shared-ui/data/graph import { useServiceRequestDetails, useTokenMetadata, + useExpireServiceRequestTx, + isServiceRequestExpired, } from '@tangle-network/tangle-shared-ui/data/services'; import type { Blueprint } from '@tangle-network/tangle-shared-ui/types/blueprint'; import { TxStatus } from '@tangle-network/tangle-shared-ui/hooks/useContractWrite'; @@ -75,6 +77,43 @@ const ServiceRequestDetailModal: FC = ({ enabled: selectedRequest !== null, }); + // Permissionless cleanup. Available once `now > createdAt + grace` and the + // request has not already been activated or rejected. The contract + // re-validates these conditions, but we gate the button to avoid wasting a + // user's gas on a guaranteed revert. + const { + execute: expireServiceRequest, + error: expireError, + reset: resetExpire, + isPending: isExpiring, + isSuccess: isExpireSuccess, + } = useExpireServiceRequestTx(); + + const canExpireRequest = useMemo(() => { + if (!contractDetails || !selectedRequest) { + return false; + } + if (contractDetails.rejected) { + return false; + } + return isServiceRequestExpired(contractDetails.createdAt); + }, [contractDetails, selectedRequest]); + + const handleExpireRequest = useCallback(async () => { + if (!selectedRequest || !canExpireRequest) { + return; + } + await expireServiceRequest?.({ requestId: selectedRequest.requestId }); + }, [canExpireRequest, expireServiceRequest, selectedRequest]); + + // After a successful expire the request is gone — close the modal so the + // parent list refetches against an invalidated `serviceRequestDetails` cache. + useEffect(() => { + if (isExpireSuccess) { + onClose(); + } + }, [isExpireSuccess, onClose]); + const { data: tokenMetadata, isLoading: isLoadingToken } = useTokenMetadata( contractDetails?.paymentToken, { @@ -271,27 +310,74 @@ const ServiceRequestDetailModal: FC = ({ /> + {expireError ? ( +
+
+ + {expireError.message || + 'Failed to expire the service request. Please try again.'} + + +
+
+ ) : null} + {!viewOnly && ( -
+
+ {canExpireRequest ? ( + + ) : null} + -
)} {viewOnly && ( -
- + ) : null} +
diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/SlashingParametersCard.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingParametersCard.tsx new file mode 100644 index 0000000000..67dfdd9364 --- /dev/null +++ b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingParametersCard.tsx @@ -0,0 +1,159 @@ +/** + * Read-only summary of the protocol-level slashing parameters returned by + * `getSlashConfig`. Shipped with tnt-core v0.13.0 — exposes the fields that + * govern dispute lifecycle (disputeBond, disputeResolutionDeadline) and the + * caps that gate proposal flow (maxSlashBps, maxPendingSlashesPerOperator). + * + * Operators and slash proposers both benefit from seeing these up front so + * they don't waste a simulation on a guaranteed revert. + */ + +import { Card } from '@tangle-network/sandbox-ui/primitives'; +import { formatUnits } from 'viem'; +import { Text } from '../../../../components/Text'; + +const SECONDS_PER_HOUR = 60 * 60; +const SECONDS_PER_DAY = SECONDS_PER_HOUR * 24; + +const formatDuration = (seconds: bigint): string => { + const total = Number(seconds); + if (!Number.isFinite(total) || total <= 0) { + return '—'; + } + + if (total >= SECONDS_PER_DAY) { + const days = total / SECONDS_PER_DAY; + const rounded = Math.round(days * 10) / 10; + return `${rounded.toLocaleString(undefined, { + maximumFractionDigits: 1, + })} day${rounded === 1 ? '' : 's'}`; + } + + if (total >= SECONDS_PER_HOUR) { + const hours = total / SECONDS_PER_HOUR; + const rounded = Math.round(hours * 10) / 10; + return `${rounded.toLocaleString(undefined, { + maximumFractionDigits: 1, + })} hour${rounded === 1 ? '' : 's'}`; + } + + return `${total.toLocaleString()} second${total === 1 ? '' : 's'}`; +}; + +// Trim trailing zeros so we don't show "0.000000000000000000 ETH". +const formatEthAmount = (wei: bigint): string => { + const formatted = formatUnits(wei, 18); + if (!formatted.includes('.')) return formatted; + return formatted.replace(/\.?0+$/, ''); +}; + +const formatBps = (bps: number): string => { + const percent = bps / 100; + return `${bps.toLocaleString()} bps (${percent.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + })}%)`; +}; + +export interface SlashingParametersCardProps { + /** + * Active SlashConfig from `getSlashConfig`. Undefined while the read is in + * flight; rendered as a skeleton in that case. + */ + config: + | { + disputeWindow: bigint; + instantSlashEnabled: boolean; + maxSlashBps: number; + disputeResolutionDeadline: bigint; + disputeBond: bigint; + maxPendingSlashesPerOperator: number; + } + | undefined; + isLoading: boolean; +} + +const SlashingParametersCard = ({ + config, + isLoading, +}: SlashingParametersCardProps) => { + if (isLoading || !config) { + return ( + + + Loading slashing parameters... + + + ); + } + + return ( + +
+ + Slashing parameters + + + Protocol-wide settings from the active SlashConfig. Proposals, + disputes, and execution all enforce these. + +
+
+
+ + Maximum slash per proposal + + + {formatBps(config.maxSlashBps)} + +
+
+ + Dispute window + + + {formatDuration(config.disputeWindow)} + +
+
+ + Dispute resolution deadline + + + {formatDuration(config.disputeResolutionDeadline)} + +
+
+ + Required dispute bond + + + {config.disputeBond > BigInt(0) + ? `${formatEthAmount(config.disputeBond)} ETH` + : 'None'} + +
+
+ + Max pending slashes per operator + + + {config.maxPendingSlashesPerOperator > 0 + ? config.maxPendingSlashesPerOperator.toLocaleString() + : 'Unlimited'} + +
+
+ + Instant slash + + + {config.instantSlashEnabled ? 'Enabled' : 'Disabled'} + +
+
+
+ ); +}; + +export default SlashingParametersCard; diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/modals/DisputeSlashModal.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/modals/DisputeSlashModal.tsx index acaf7b66c5..06ef5c2b35 100644 --- a/apps/tangle-cloud/src/pages/operators/manage/components/modals/DisputeSlashModal.tsx +++ b/apps/tangle-cloud/src/pages/operators/manage/components/modals/DisputeSlashModal.tsx @@ -1,4 +1,5 @@ import type { ChangeEvent } from 'react'; +import { formatUnits, zeroAddress } from 'viem'; import { SlashActionPermissions, SlashDisputeEligibility, @@ -24,6 +25,14 @@ import { const shortenHex = (value: string) => value.length <= 12 ? value : `${value.slice(0, 6)}...${value.slice(-4)}`; +// Trim trailing zeros so we don't show "0.000000000000000000 ETH" or +// "1.500000000000000000 ETH" in the bond row. +const formatEthAmount = (wei: bigint): string => { + const formatted = formatUnits(wei, 18); + if (!formatted.includes('.')) return formatted; + return formatted.replace(/\.?0+$/, ''); +}; + interface DisputeSlashModalProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -39,6 +48,17 @@ interface DisputeSlashModalProps { onConfirm: () => void; errorMessage: string | null; onDismissError: () => void; + /** + * msg.value the contract will require for disputeSlash. Read from the active + * SlashConfig and surfaced here so the user knows what bond they are posting + * before signing. + */ + disputeBond: bigint; + /** + * Seconds remaining until the dispute resolution deadline. Only meaningful + * when the slash is already in `Disputed` status; null otherwise. + */ + disputeResolutionSecondsRemaining: number | null; } const DisputeSlashModal = ({ @@ -56,7 +76,14 @@ const DisputeSlashModal = ({ onConfirm, errorMessage, onDismissError, + disputeBond, + disputeResolutionSecondsRemaining, }: DisputeSlashModalProps) => { + const isAlreadyDisputed = selectedSlash?.status === 'Disputed'; + const hasKnownDisputer = + !!selectedSlash && + selectedSlash.disputer.toLowerCase() !== zeroAddress.toLowerCase(); + return ( @@ -130,6 +157,41 @@ const DisputeSlashModal = ({ {selectedSlash?.evidence ?? '-'} + + Required Dispute Bond: + + + {disputeBond > BigInt(0) + ? `${formatEthAmount(disputeBond)} ETH (refunded if dispute upheld)` + : 'No bond required'} + + {isAlreadyDisputed && hasKnownDisputer ? ( + <> + + Disputer: + + + {selectedSlash ? shortenHex(selectedSlash.disputer) : '-'} + + + ) : null} + {isAlreadyDisputed && + disputeResolutionSecondsRemaining !== null ? ( + <> + + Resolution Deadline: + + + {disputeResolutionSecondsRemaining > 0 + ? formatTimeRemaining(disputeResolutionSecondsRemaining) + : 'Deadline passed'} + + + ) : null}
diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/modals/ProposeSlashModal.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/modals/ProposeSlashModal.tsx index 8b8822172e..6f717a5032 100644 --- a/apps/tangle-cloud/src/pages/operators/manage/components/modals/ProposeSlashModal.tsx +++ b/apps/tangle-cloud/src/pages/operators/manage/components/modals/ProposeSlashModal.tsx @@ -30,6 +30,13 @@ interface ProposeSlashModalProps { canSubmitPropose: boolean; isSubmitting: boolean; onConfirm: () => void; + /** + * Active SlashConfig.maxSlashBps cap (0..10_000). Used both as the upper + * bound in the input placeholder and to short-circuit out-of-range BPS + * before the user pays for a simulation. Undefined while the config is + * loading; in that case we fall back to 10_000. + */ + maxSlashBps: number | undefined; } const ProposeSlashModal = ({ @@ -53,7 +60,18 @@ const ProposeSlashModal = ({ canSubmitPropose, isSubmitting, onConfirm, + maxSlashBps, }: ProposeSlashModalProps) => { + // Hard ceiling defined by the contract; SlashConfig.maxSlashBps is the + // soft (admin-configurable) cap which is always <= 10_000. + const effectiveMaxBps = + maxSlashBps !== undefined && maxSlashBps > 0 ? maxSlashBps : 10_000; + const maxBpsLabel = effectiveMaxBps.toLocaleString(); + const maxPercentLabel = (effectiveMaxBps / 100).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }); + return ( @@ -122,15 +140,24 @@ const ProposeSlashModal = ({
- Slash BPS (1 - 10000) + Slash BPS (1 - {maxBpsLabel}) + {maxSlashBps !== undefined ? ( + + Protocol cap: {maxBpsLabel} bps ({maxPercentLabel}%). + Proposals above the cap are rejected on-chain. + + ) : null}
diff --git a/apps/tangle-cloud/src/pages/operators/manage/hooks/useSlashProposalForm.ts b/apps/tangle-cloud/src/pages/operators/manage/hooks/useSlashProposalForm.ts index 35a01cd0f0..8ef4453780 100644 --- a/apps/tangle-cloud/src/pages/operators/manage/hooks/useSlashProposalForm.ts +++ b/apps/tangle-cloud/src/pages/operators/manage/hooks/useSlashProposalForm.ts @@ -25,11 +25,19 @@ export interface UseSlashProposalFormResult { interface UseSlashProposalFormOptions { proposableServices: ProposableService[] | undefined; proposeStatus: 'idle' | 'pending' | 'success' | 'error'; + /** + * Active SlashConfig.maxSlashBps cap. Slash proposals above this value are + * rejected on-chain, so we surface the violation client-side before the + * user pays for a simulation. Defaults to the contract hard ceiling + * (10_000) when the config is still loading. + */ + maxSlashBps?: number; } const useSlashProposalForm = ({ proposableServices, proposeStatus, + maxSlashBps, }: UseSlashProposalFormOptions): UseSlashProposalFormResult => { const [proposeServiceId, setProposeServiceId] = useState(''); const [proposeOperator, setProposeOperator] = useState(''); @@ -76,6 +84,16 @@ const useSlashProposalForm = ({ return 'Slash BPS must be an integer between 1 and 10000.'; } + // Enforce the active SlashConfig.maxSlashBps cap once it has loaded so we + // fail fast in the UI rather than during simulation. + if ( + maxSlashBps !== undefined && + maxSlashBps > 0 && + slashBps > maxSlashBps + ) { + return `Slash BPS exceeds protocol cap of ${maxSlashBps.toLocaleString()} bps.`; + } + if (evidenceNormalization.error) { return evidenceNormalization.error; } @@ -83,6 +101,7 @@ const useSlashProposalForm = ({ return null; }, [ evidenceNormalization.error, + maxSlashBps, proposeOperator, proposeServiceId, proposeSlashBps, diff --git a/apps/tangle-cloud/src/pages/operators/manage/page.tsx b/apps/tangle-cloud/src/pages/operators/manage/page.tsx index 8b51a8b9c1..9e5f36e67a 100644 --- a/apps/tangle-cloud/src/pages/operators/manage/page.tsx +++ b/apps/tangle-cloud/src/pages/operators/manage/page.tsx @@ -34,6 +34,7 @@ import { useServicesByOperator, useActiveServiceMemberships, useSlashProposals, + useSlashConfig, useDisputeSlashTx, useCancelSlashTx, useProposeSlashTx, @@ -75,6 +76,7 @@ import useChainClock from './hooks/useChainClock'; import useSlashProposalForm from './hooks/useSlashProposalForm'; import useSlashActions from './hooks/useSlashActions'; import SlashingSummaryCards from './components/SlashingSummaryCards'; +import SlashingParametersCard from './components/SlashingParametersCard'; import SlashingTabsTable from './components/SlashingTabsTable'; import ProposeSlashModal from './components/modals/ProposeSlashModal'; import DisputeMessageModal from './components/modals/DisputeMessageModal'; @@ -333,6 +335,9 @@ const Page: FC = () => { enabled: isConnected, proposals: slashProposals, }); + const { data: slashConfig, isLoading: loadingSlashConfig } = useSlashConfig({ + enabled: isConnected, + }); // Transaction hooks (registration) const { unregisterOperator, status: unregisterStatus } = @@ -391,6 +396,7 @@ const Page: FC = () => { } = useSlashProposalForm({ proposableServices, proposeStatus, + maxSlashBps: slashConfig?.maxSlashBps, }); const executableSlashIdSet = useMemo(() => { @@ -507,6 +513,22 @@ const Page: FC = () => { return buildSlashTimeline(selectedSlash, nowUnixSeconds); }, [selectedSlash, nowUnixSeconds]); + const disputeBond = slashConfig?.disputeBond ?? BigInt(0); + const slashConfigMaxSlashBps = slashConfig?.maxSlashBps; + + // Only meaningful when the slash is already in `Disputed` state and we have + // an authoritative on-chain disputeDeadline. Returns null otherwise so the + // modal can decide not to render the row. + const selectedSlashDisputeResolutionSecondsRemaining = useMemo(() => { + if (!selectedSlash || selectedSlash.status !== 'Disputed') { + return null; + } + if (selectedSlash.disputeDeadline === BigInt(0)) { + return null; + } + return Number(selectedSlash.disputeDeadline) - nowUnixSeconds; + }, [selectedSlash, nowUnixSeconds]); + const nearestPendingSlash = useMemo(() => { const pendingAgainstMe = againstMe.filter( (slash) => slash.status === 'Pending', @@ -1211,6 +1233,11 @@ const Page: FC = () => { nearestPendingSlashEligibility={nearestPendingSlashEligibility} /> + + {clockError ? ( @@ -1407,6 +1434,7 @@ const Page: FC = () => { canSubmitPropose={canSubmitPropose} isSubmitting={proposeStatus === 'pending'} onConfirm={() => void handleProposeSlash()} + maxSlashBps={slashConfigMaxSlashBps} /> { onConfirm={() => void handleDispute()} errorMessage={actionError.dispute} onDismissError={() => clearActionError('dispute')} + disputeBond={disputeBond} + disputeResolutionSecondsRemaining={ + selectedSlashDisputeResolutionSecondsRemaining + } /> { + // Tuple layout from getSlashProposal (tnt-core v0.13.0): + // 0 serviceId, 1 operator, 2 proposer, 3 slashBps, 4 effectiveSlashBps, + // 5 evidence, 6 proposedAt, 7 executeAfter, 8 status, 9 disputeReason, + // 10 disputer, 11 disputeBond, 12 disputedAt, 13 disputeDeadline. const serviceId = proposal?.serviceId !== undefined ? BigInt(proposal.serviceId.toString()) @@ -767,6 +787,18 @@ const normalizeOnChainSlashProposal = ( const disputeReason = (proposal?.disputeReason ?? proposal?.[9] ?? null) as | string | null; + const disputer = (proposal?.disputer ?? + proposal?.[10] ?? + zeroAddress) as Address; + const disputeBond = BigInt( + proposal?.disputeBond?.toString() ?? proposal?.[11]?.toString() ?? 0, + ); + const disputedAt = BigInt( + proposal?.disputedAt?.toString() ?? proposal?.[12]?.toString() ?? 0, + ); + const disputeDeadline = BigInt( + proposal?.disputeDeadline?.toString() ?? proposal?.[13]?.toString() ?? 0, + ); return { id: slashId, @@ -784,6 +816,10 @@ const normalizeOnChainSlashProposal = ( status: parseSlashStatus(statusValue), disputeReason, cancelReason: null, + disputer, + disputeBond, + disputedAt, + disputeDeadline, }; }; diff --git a/libs/tangle-shared-ui/src/data/services/index.ts b/libs/tangle-shared-ui/src/data/services/index.ts index e0b918b1d7..4b27e39138 100644 --- a/libs/tangle-shared-ui/src/data/services/index.ts +++ b/libs/tangle-shared-ui/src/data/services/index.ts @@ -55,6 +55,15 @@ export { type UseFundServiceTxOptions, } from './useFundServiceTx'; +export { + useExpireServiceRequestTx, + isServiceRequestExpired, + getServiceRequestExpiryEligibleAt, + REQUEST_EXPIRY_GRACE_PERIOD_SECONDS, + type ExpireServiceRequestParams, + type UseExpireServiceRequestTxOptions, +} from './useExpireServiceRequest'; + export { useBillSubscriptionTx, type BillSubscriptionParams, diff --git a/libs/tangle-shared-ui/src/data/services/useExpireServiceRequest.ts b/libs/tangle-shared-ui/src/data/services/useExpireServiceRequest.ts new file mode 100644 index 0000000000..f53db691a0 --- /dev/null +++ b/libs/tangle-shared-ui/src/data/services/useExpireServiceRequest.ts @@ -0,0 +1,113 @@ +/** + * Hook for permissionlessly expiring a stale service request. + * + * `expireServiceRequest` is callable by anyone once `block.timestamp > + * req.createdAt + REQUEST_EXPIRY_GRACE_PERIOD`. Calling it refunds the + * requester and unlocks the operator candidates, so it is safe — and + * incentive-aligned — to expose as a "Clean up expired request" action. + * + * This hook only handles the on-chain write. Callers are responsible for + * gating the button on the grace period using + * REQUEST_EXPIRY_GRACE_PERIOD_SECONDS, and for invalidating any request / + * service queries on success. + */ + +import { useChainId } from 'wagmi'; +import { useQueryClient } from '@tanstack/react-query'; +import { getContractsByChainId } from '@tangle-network/dapp-config/contracts'; +import useContractWrite, { TxStatus } from '../../hooks/useContractWrite'; +import TangleABI from '../../abi/tangle'; + +export { TxStatus }; + +/** + * Mirrors `ProtocolConfig.REQUEST_EXPIRY_GRACE_PERIOD` (1 hour) from tnt-core. + * The contract allows admins to override `_requestExpiryGracePeriod`, but no + * getter is currently exposed for it; this constant reflects the protocol + * default and is the conservative gate to use in the UI. + */ +export const REQUEST_EXPIRY_GRACE_PERIOD_SECONDS = BigInt(60 * 60); + +export interface ExpireServiceRequestParams { + requestId: bigint; +} + +export interface UseExpireServiceRequestTxOptions { + onSuccess?: () => void; + onError?: (error: Error) => void; +} + +/** + * Returns true iff `now > createdAt + grace`. `now` defaults to the system + * clock; pass an externally-clocked value when you want the gate to advance + * at the same cadence as the rest of the page (e.g. a `useChainClock` tick). + */ +export const isServiceRequestExpired = ( + createdAt: bigint, + nowUnixSeconds = BigInt(Math.floor(Date.now() / 1000)), + graceSeconds = REQUEST_EXPIRY_GRACE_PERIOD_SECONDS, +): boolean => nowUnixSeconds > createdAt + graceSeconds; + +/** + * Returns the seconds remaining until the request can be expired. Negative + * (or zero) when the request is already eligible for expiry. + */ +export const getServiceRequestExpiryEligibleAt = ( + createdAt: bigint, + graceSeconds = REQUEST_EXPIRY_GRACE_PERIOD_SECONDS, +): bigint => createdAt + graceSeconds; + +export const useExpireServiceRequestTx = ( + options?: UseExpireServiceRequestTxOptions, +) => { + const chainId = useChainId(); + const queryClient = useQueryClient(); + + const hook = useContractWrite( + TangleABI, + (params: ExpireServiceRequestParams) => { + let contracts: ReturnType; + try { + contracts = getContractsByChainId(chainId); + } catch { + throw new Error('Tangle contract not available on this network'); + } + + return { + address: contracts.tangle, + abi: TangleABI, + functionName: 'expireServiceRequest' as const, + args: [params.requestId] as const, + }; + }, + { + txName: 'expire service request', + txDetails: (params) => + new Map([['Request ID', params.requestId.toString()]]), + getSuccessMessage: (params) => + `Service request #${params.requestId} expired and refunded.`, + onSuccess: () => { + // The expire path flips `req.rejected = true` and refunds escrow, so + // anything that reads the request, the surrounding service, or the + // requester's balance must refetch. + queryClient.invalidateQueries({ queryKey: ['serviceRequestDetails'] }); + queryClient.invalidateQueries({ queryKey: ['serviceRequests'] }); + queryClient.invalidateQueries({ queryKey: ['services'] }); + options?.onSuccess?.(); + }, + onError: options?.onError, + }, + ); + + return { + execute: hook.execute, + status: hook.status, + error: hook.error, + reset: hook.reset, + txHash: hook.txHash, + isSuccess: hook.isSuccess, + isPending: hook.isLoading, + }; +}; + +export default useExpireServiceRequestTx; diff --git a/scripts/sync-tnt-core-assets.mjs b/scripts/sync-tnt-core-assets.mjs index 90aa8d0591..35a38d350c 100644 --- a/scripts/sync-tnt-core-assets.mjs +++ b/scripts/sync-tnt-core-assets.mjs @@ -1,6 +1,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, '..'); @@ -41,7 +42,9 @@ const syncAbis = (tntCoreDir) => { const mappings = [ { source: 'ITangleFull.json', target: 'libs/tangle-shared-ui/src/abi/tangle.ts' }, { source: 'IMultiAssetDelegation.json', target: 'libs/tangle-shared-ui/src/abi/multiAssetDelegation.ts' }, - { source: 'IOperatorStatusRegistry.json', target: 'libs/tangle-shared-ui/src/abi/operatorStatusRegistry.ts' }, + // tnt-core v0.13.0 ships the implementation ABI (no `I` prefix) — it carries + // the same external surface the dapp needs and the interface JSON is no longer emitted. + { source: 'OperatorStatusRegistry.json', target: 'libs/tangle-shared-ui/src/abi/operatorStatusRegistry.ts' }, { source: 'IBlueprintServiceManager.json', target: 'libs/tangle-shared-ui/src/abi/blueprintServiceManager.ts' }, ]; @@ -75,10 +78,31 @@ const syncFixtures = (tntCoreDir) => { } }; +const formatGeneratedAbis = () => { + // Run prettier so the generated `as const` ABIs match the repo style + // (single quotes, trailing commas) instead of raw JSON.stringify output. + const targets = [ + 'libs/tangle-shared-ui/src/abi/tangle.ts', + 'libs/tangle-shared-ui/src/abi/multiAssetDelegation.ts', + 'libs/tangle-shared-ui/src/abi/operatorStatusRegistry.ts', + 'libs/tangle-shared-ui/src/abi/blueprintServiceManager.ts', + ].map((relative) => resolve(repoRoot, relative)); + + const result = spawnSync('npx', ['prettier', '--write', '--log-level=warn', ...targets], { + cwd: repoRoot, + stdio: 'inherit', + }); + + if (result.status !== 0) { + console.warn('[sync] prettier formatting exited with a non-zero status; please run `yarn format` manually.'); + } +}; + const main = () => { const tntCoreDir = resolveTntCoreDir(); syncAbis(tntCoreDir); syncFixtures(tntCoreDir); + formatGeneratedAbis(); }; main(); diff --git a/storybook-migration-summary.md b/storybook-migration-summary.md index 67a93ba6ce..26337c71e9 100644 --- a/storybook-migration-summary.md +++ b/storybook-migration-summary.md @@ -59,4 +59,6 @@ Please read the [Storybook 7.0.0 release article](https://storybook.js.org/blog/ official [Storybook 7.0.0 migration guide](https://storybook.js.org/docs/react/migration-guide) for more information. -You can also read the docs for the [@nx/storybook:migrate-7 generator](https://nx.dev/packages/storybook/generators/migrate-7) and our [Storybook 7 setup guide](https://nx.dev/packages/storybook/documents/storybook-7-setup). +For Nx-specific Storybook setup, see the current Nx documentation at https://nx.dev (the +historical `@nx/storybook:migrate-7` generator and `storybook-7-setup` document URLs have +been retired upstream).