From 644b3449cb7c385ca204e15a97357c2960be155a Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 2 Jul 2026 21:57:12 -0600 Subject: [PATCH] feat(contracts): tnt-core 0.18 compatibility behind a per-chain revision flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.18 chains are greenfield redeploys with three consumer-visible breaks the dapp would hit silently: - getBlueprint dropped operatorCount from the struct (7->6 fields): the fixed-ABI decode throws and the catalog chain-fallback renders empty. - MultiAssetDelegation LockInfo's expiryBlock slot now carries a unix TIMESTAMP (same position/type, decode silently succeeds): comparing it against a block number reads 'locked for decades' and locked balances never become withdrawable in the UI. - Display prose is event-sourced (indexer path; storage zeroed). Add getTntCoreRevisionByChainId ('legacy' | 'v018') to dapp-config and key the three divergent reads on it: readBlueprintCore() picks the right getBlueprint tuple + blueprintOperatorCount view (both fetch sites + the binary-upgrade owner gate), and the withdraw lock check compares against now-seconds on v018. Every wired chain is 'legacy' today (Base Sepolia probed live: blueprintDefinitionHash reverts = pre-#193), so this is zero behavior change until a chain's addresses are flipped to a 0.18 redeploy — flip the revision in that same change. --- .../binaryUpgrade/BlueprintVersionsPanel.tsx | 16 +-- .../src/pages/staking/withdraw/index.tsx | 18 ++- libs/dapp-config/src/contracts.ts | 29 +++++ .../data/blueprints/fetchBlueprintsOnChain.ts | 39 ++----- .../src/data/blueprints/readBlueprintCore.ts | 103 ++++++++++++++++++ 5 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 libs/tangle-shared-ui/src/data/blueprints/readBlueprintCore.ts diff --git a/apps/tangle-cloud/src/components/binaryUpgrade/BlueprintVersionsPanel.tsx b/apps/tangle-cloud/src/components/binaryUpgrade/BlueprintVersionsPanel.tsx index 20ccc9c67..7cab4cfaf 100644 --- a/apps/tangle-cloud/src/components/binaryUpgrade/BlueprintVersionsPanel.tsx +++ b/apps/tangle-cloud/src/components/binaryUpgrade/BlueprintVersionsPanel.tsx @@ -24,8 +24,8 @@ import { type Auditor, } from '@tangle-network/tangle-shared-ui/blueprintApps/trustScore'; import { getContractsByChainId } from '@tangle-network/dapp-config/contracts'; +import { readBlueprintCore } from '@tangle-network/tangle-shared-ui/data/blueprints/readBlueprintCore'; import type { Address } from 'viem'; -import TangleABI from '@tangle-network/tangle-shared-ui/abi/tangle'; import TrustScoreGauge from './TrustScoreGauge'; import AttestationBadge from './AttestationBadge'; @@ -94,12 +94,14 @@ const useBlueprintOwner = (blueprintId: bigint | undefined) => { try { const contracts = getContractsByChainId(chainId); const tangle = contracts.tangle as Address; - const result = (await publicClient.readContract({ - address: tangle, - abi: TangleABI, - functionName: 'getBlueprint', - args: [blueprintId], - })) as { owner: Address }; + // Revision-aware: the 0.18 Blueprint struct dropped operatorCount, + // so a fixed-ABI getBlueprint decode throws on the other revision. + const result = await readBlueprintCore( + publicClient, + tangle, + blueprintId, + chainId, + ); return result.owner; } catch { return null; diff --git a/apps/tangle-dapp/src/pages/staking/withdraw/index.tsx b/apps/tangle-dapp/src/pages/staking/withdraw/index.tsx index 06353dc80..de7e630f3 100644 --- a/apps/tangle-dapp/src/pages/staking/withdraw/index.tsx +++ b/apps/tangle-dapp/src/pages/staking/withdraw/index.tsx @@ -52,7 +52,10 @@ import filterBy from '@tangle-network/tangle-shared-ui/utils/filterBy'; import { chainsConfig } from '@tangle-network/dapp-config/chains'; import AssetListItem from '../../../components/Lists/AssetListItem'; import MULTI_ASSET_DELEGATION_ABI from '@tangle-network/tangle-shared-ui/abi/multiAssetDelegation'; -import { getContractsByChainId } from '@tangle-network/dapp-config/contracts'; +import { + getContractsByChainId, + getTntCoreRevisionByChainId, +} from '@tangle-network/dapp-config/contracts'; import { useResilientReadContract } from '@tangle-network/tangle-shared-ui/hooks/useResilientReadContract'; import { useResilientReadContracts } from '@tangle-network/tangle-shared-ui/hooks/useResilientReadContracts'; @@ -296,15 +299,24 @@ const StakingWithdrawForm: FC = () => { expiryBlock: bigint; }>; + // tnt-core 0.18 redefined the LockInfo slot as a unix TIMESTAMP while + // keeping the same position/type, so the decode silently succeeds on + // both revisions — the comparison basis is what must branch. Comparing + // a timestamp against a block number reads "locked for decades". + const nowSeconds = BigInt(Math.floor(Date.now() / 1000)); const locked = locks.reduce((sum, lock) => { - return lock.expiryBlock > currentBlockNumber ? sum + lock.amount : sum; + const stillLocked = + getTntCoreRevisionByChainId(chainId) === 'v018' + ? lock.expiryBlock > nowSeconds + : lock.expiryBlock > currentBlockNumber; + return stillLocked ? sum + lock.amount : sum; }, BigInt(0)); map.set(token.toLowerCase(), locked); }); return map; - }, [currentBlockNumber, lockResults, tokenAddresses]); + }, [chainId, currentBlockNumber, lockResults, tokenAddresses]); const selectedAssetId = watch('assetId'); const amount = watch('amount'); diff --git a/libs/dapp-config/src/contracts.ts b/libs/dapp-config/src/contracts.ts index c2a4e2754..b725f3677 100644 --- a/libs/dapp-config/src/contracts.ts +++ b/libs/dapp-config/src/contracts.ts @@ -134,6 +134,35 @@ export const ETHEREUM_HOLESKY_CONTRACTS: ContractAddresses = { blueprintAuditors: '0x0000000000000000000000000000000000000000', }; +/** + * Which tnt-core contract revision a chain runs. + * + * - `legacy`: pre-0.18 deployments — `getBlueprint` returns `operatorCount` + * inside the struct, `getOperatorPreferences` persists `rpcAddress`, and + * MultiAssetDelegation `LockInfo.expiryBlock` is a BLOCK NUMBER. + * - `v018`: tnt-core 0.18 greenfield redeploys — `operatorCount` moved to the + * `blueprintOperatorCount(uint64)` view (struct is 6 fields; decoding the + * legacy 7-field tuple throws), display prose is event-sourced from + * `BlueprintDefinitionRecorded`, and the `LockInfo` slot named + * `expiryBlock` carries a unix TIMESTAMP. + * + * Flip a chain to `v018` in the same change that updates its addresses to + * the redeployed contracts — never separately. + */ +export type TntCoreRevision = 'legacy' | 'v018'; + +export const getTntCoreRevisionByChainId = ( + chainId: number, +): TntCoreRevision => { + switch (chainId) { + // Every currently-wired chain runs pre-0.18 contracts (Base Sepolia is + // pre-#193; local anvil is post-#193 but pre-#194, which for every + // surface the dapp branches on behaves like `legacy`). + default: + return 'legacy'; + } +}; + // Get contracts by chain ID export const getContractsByChainId = (chainId: number): ContractAddresses => { switch (chainId) { diff --git a/libs/tangle-shared-ui/src/data/blueprints/fetchBlueprintsOnChain.ts b/libs/tangle-shared-ui/src/data/blueprints/fetchBlueprintsOnChain.ts index d45476826..25520b1d1 100644 --- a/libs/tangle-shared-ui/src/data/blueprints/fetchBlueprintsOnChain.ts +++ b/libs/tangle-shared-ui/src/data/blueprints/fetchBlueprintsOnChain.ts @@ -1,6 +1,7 @@ import type { Address, PublicClient } from 'viem'; import { getContractsByChainId } from '@tangle-network/dapp-config/contracts'; import TANGLE_ABI from '../../abi/tangle'; +import { readBlueprintCore } from './readBlueprintCore'; import type { Blueprint } from '../graphql/useBlueprints'; /** @@ -61,18 +62,7 @@ export const fetchBlueprintsOnChain = async ( ids.map(async (id) => { try { const [blueprint, mutableMeta] = await Promise.all([ - publicClient.readContract({ - address: tangleAddress, - abi: TANGLE_ABI, - functionName: 'getBlueprint', - args: [id], - }) as Promise<{ - owner: Address; - manager: Address; - createdAt: bigint; - operatorCount: number; - active: boolean; - }>, + readBlueprintCore(publicClient, tangleAddress, id, chainId), publicClient.readContract({ address: tangleAddress, abi: TANGLE_ABI, @@ -104,14 +94,14 @@ export const fetchBlueprintsOnChain = async ( ? definition.metadataHash : null, active: blueprint.active, - createdAt: BigInt(blueprint.createdAt), + createdAt: blueprint.createdAt, // `updatedAt` is tracked off-chain by the indexer (event log of // updateBlueprint calls). Without that, the best we can do is // surface the original createdAt so downstream sorts behave // predictably; the value is honest about "not seen any // updates since creation". - updatedAt: BigInt(blueprint.createdAt), - operatorCount: BigInt(blueprint.operatorCount), + updatedAt: blueprint.createdAt, + operatorCount: blueprint.operatorCount, }; return result; } catch { @@ -150,18 +140,7 @@ export const fetchBlueprintByIdOnChain = async ( try { const [blueprint, mutableMeta] = await Promise.all([ - publicClient.readContract({ - address: tangleAddress, - abi: TANGLE_ABI, - functionName: 'getBlueprint', - args: [blueprintId], - }) as Promise<{ - owner: Address; - manager: Address; - createdAt: bigint; - operatorCount: number; - active: boolean; - }>, + readBlueprintCore(publicClient, tangleAddress, blueprintId, chainId), publicClient.readContract({ address: tangleAddress, abi: TANGLE_ABI, @@ -193,9 +172,9 @@ export const fetchBlueprintByIdOnChain = async ( ? definition.metadataHash : null, active: blueprint.active, - createdAt: BigInt(blueprint.createdAt), - updatedAt: BigInt(blueprint.createdAt), - operatorCount: BigInt(blueprint.operatorCount), + createdAt: blueprint.createdAt, + updatedAt: blueprint.createdAt, + operatorCount: blueprint.operatorCount, }; } catch { return null; diff --git a/libs/tangle-shared-ui/src/data/blueprints/readBlueprintCore.ts b/libs/tangle-shared-ui/src/data/blueprints/readBlueprintCore.ts new file mode 100644 index 000000000..025202aa1 --- /dev/null +++ b/libs/tangle-shared-ui/src/data/blueprints/readBlueprintCore.ts @@ -0,0 +1,103 @@ +import type { Address, PublicClient } from 'viem'; +import { + getTntCoreRevisionByChainId, + type TntCoreRevision, +} from '@tangle-network/dapp-config/contracts'; +import TANGLE_ABI from '../../abi/tangle'; + +/** + * tnt-core 0.18 `Types.Blueprint` — `operatorCount` was removed from the + * struct (derived from the operator set via `blueprintOperatorCount`), so the + * legacy 7-field tuple in the synced ABI throws on 0.18 returndata and this + * 6-field variant throws on legacy returndata. Selection is keyed by the + * per-chain `TntCoreRevision`; do not merge the two entries into one ABI. + */ +const GET_BLUEPRINT_V018_ABI = [ + { + type: 'function', + name: 'getBlueprint', + stateMutability: 'view', + inputs: [{ name: 'blueprintId', type: 'uint64' }], + outputs: [ + { + name: '', + type: 'tuple', + components: [ + { name: 'owner', type: 'address' }, + { name: 'manager', type: 'address' }, + { name: 'createdAt', type: 'uint64' }, + { name: 'membership', type: 'uint8' }, + { name: 'pricing', type: 'uint8' }, + { name: 'active', type: 'bool' }, + ], + }, + ], + }, +] as const; + +export type BlueprintCore = { + owner: Address; + manager: Address; + createdAt: bigint; + active: boolean; + operatorCount: bigint; +}; + +/** + * Revision-aware read of a blueprint's core fields. + * + * `legacy`: one `getBlueprint` call; `operatorCount` comes from the struct. + * `v018`: `getBlueprint` (6-field tuple) + the `blueprintOperatorCount` view. + */ +export const readBlueprintCore = async ( + publicClient: PublicClient, + tangleAddress: Address, + blueprintId: bigint, + chainId: number, +): Promise => { + const revision: TntCoreRevision = getTntCoreRevisionByChainId(chainId); + + if (revision === 'legacy') { + const blueprint = (await publicClient.readContract({ + address: tangleAddress, + abi: TANGLE_ABI, + functionName: 'getBlueprint', + args: [blueprintId], + })) as { + owner: Address; + manager: Address; + createdAt: bigint; + operatorCount: number; + active: boolean; + }; + return { + owner: blueprint.owner, + manager: blueprint.manager, + createdAt: BigInt(blueprint.createdAt), + active: blueprint.active, + operatorCount: BigInt(blueprint.operatorCount), + }; + } + + const [blueprint, operatorCount] = await Promise.all([ + publicClient.readContract({ + address: tangleAddress, + abi: GET_BLUEPRINT_V018_ABI, + functionName: 'getBlueprint', + args: [blueprintId], + }), + publicClient.readContract({ + address: tangleAddress, + abi: TANGLE_ABI, + functionName: 'blueprintOperatorCount', + args: [blueprintId], + }) as Promise, + ]); + return { + owner: blueprint.owner, + manager: blueprint.manager, + createdAt: BigInt(blueprint.createdAt), + active: blueprint.active, + operatorCount, + }; +};