#!/usr/bin/env bash # Generate an operator-grade GRU v2 public deployment queue across Wave 1 # transport activation, public-chain cW pool deployment, and protocol staging. # # Usage: # bash scripts/verify/check-gru-v2-deployment-queue.sh # bash scripts/verify/check-gru-v2-deployment-queue.sh --json # bash scripts/verify/check-gru-v2-deployment-queue.sh --write-explorer-config set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" export PROJECT_ROOT OUTPUT_JSON=0 WRITE_EXPLORER_CONFIG=0 for arg in "$@"; do case "$arg" in --json) OUTPUT_JSON=1 ;; --write-explorer-config) WRITE_EXPLORER_CONFIG=1 ;; *) echo "Unknown argument: $arg" >&2 exit 2 ;; esac done command -v node >/dev/null 2>&1 || { echo "[FAIL] Missing required command: node" >&2 exit 1 } OUTPUT_JSON="$OUTPUT_JSON" WRITE_EXPLORER_CONFIG="$WRITE_EXPLORER_CONFIG" node <<'NODE' const fs = require('fs'); const path = require('path'); const root = process.env.PROJECT_ROOT; const outputJson = process.env.OUTPUT_JSON === '1'; const writeExplorerConfig = process.env.WRITE_EXPLORER_CONFIG === '1'; const explorerConfigPath = path.join( root, 'explorer-monorepo/backend/api/rest/config/metamask/GRU_V2_DEPLOYMENT_QUEUE.json' ); function readJson(relPath) { return JSON.parse(fs.readFileSync(path.join(root, relPath), 'utf8')); } const rollout = readJson('config/gru-global-priority-currency-rollout.json'); const manifest = readJson('config/gru-iso4217-currency-manifest.json'); const transport = readJson('config/gru-transport-active.json'); const deployment = readJson('cross-chain-pmm-lps/config/deployment-status.json'); const poolMatrix = readJson('cross-chain-pmm-lps/config/pool-matrix.json'); const protocolPlan = readJson('config/gru-v2-public-protocol-rollout-plan.json'); const mapping = readJson('config/token-mapping-multichain.json'); const routingRegistry = readJson('config/routing-registry.json'); const manifestByCode = new Map((manifest.currencies || []).map((item) => [item.code, item])); const transportSymbols = new Set((transport.enabledCanonicalTokens || []).map((item) => item.symbol)); const desiredChainIds = rollout.desiredDestinationNetworks?.evmPublicCwMeshChainIds || []; const poolMatrixTokens = new Set(poolMatrix.cwTokens || []); const cToCw = mapping.cToCwSymbolMapping || {}; const wave1Assets = (rollout.assets || []).filter((asset) => asset.wave === 'wave1'); const wave1WrappedSymbols = [...new Set(wave1Assets.flatMap((asset) => (asset.tokenForms || []).map((item) => item.wrappedSymbol)))]; const wave1CanonicalSymbols = [...new Set(wave1Assets.flatMap((asset) => (asset.tokenForms || []).map((item) => item.canonicalSymbol)))]; const poolMatrixMissingWave1 = wave1WrappedSymbols.filter((symbol) => !poolMatrixTokens.has(symbol)); const arbitrumRoute = (routingRegistry.routes || []).find((route) => route.fromChain === 138 && route.toChain === 42161 && route.asset === 'WETH'); const arbitrumHubBlocker = { active: true, fromChain: 138, viaChain: 1, toChain: 42161, currentPath: '138 -> Mainnet -> Arbitrum', sourceBridge: '0xc9901ce2Ddb6490FAA183645147a87496d8b20B6', failedTxHash: '0x97df657f0e31341ca852666766e553650531bbcc86621246d041985d7261bb07', note: (arbitrumRoute && arbitrumRoute.note) || 'Use Mainnet hub; the current Mainnet -> Arbitrum WETH9 leg is blocked.' }; function deriveRepoState(bits) { if (bits.transportActive) return 'live_transport'; if (bits.canonical138Deployed) return 'canonical_only'; if (bits.manifestPresent) return 'manifest_only'; if (bits.cToCwMapped) return 'mapping_only'; return 'backlog'; } const allAssetResults = (rollout.assets || []).map((asset) => { const manifestEntry = manifestByCode.get(asset.code); const canonicalSymbols = (asset.tokenForms || []).map((item) => item.canonicalSymbol); const wrappedSymbols = (asset.tokenForms || []).map((item) => item.wrappedSymbol); const manifestPresent = Boolean(manifestEntry); const canonical138Deployed = Boolean(manifestEntry?.status?.deployed); const transportActive = canonicalSymbols.some((symbol) => transportSymbols.has(symbol)); const cToCwMapped = canonicalSymbols.length > 0 && canonicalSymbols.every((symbol, idx) => cToCw[symbol] === wrappedSymbols[idx]); return { code: asset.code, wave: asset.wave, currentRepoState: deriveRepoState({ manifestPresent, canonical138Deployed, cToCwMapped, transportActive }) }; }); const rolloutBacklogAssets = allAssetResults.filter((item) => item.currentRepoState === 'backlog').length; function normalizePair(pair) { return String(pair || '').trim().toUpperCase(); } function poolEntryMatchesPair(entry, pair) { const normalized = normalizePair(pair); const [base, quote] = normalized.split('/'); const baseCandidate = String(entry.base || entry.base_token || '').trim().toUpperCase(); const quoteCandidate = String(entry.quote || entry.quote_token || '').trim().toUpperCase(); return baseCandidate === base && quoteCandidate === quote; } const assetQueue = wave1Assets.map((asset) => { const manifestEntry = manifestByCode.get(asset.code); const canonicalSymbols = (asset.tokenForms || []).map((item) => item.canonicalSymbol); const wrappedSymbols = (asset.tokenForms || []).map((item) => item.wrappedSymbol); const transportActive = canonicalSymbols.some((symbol) => transportSymbols.has(symbol)); const coveredByPoolMatrix = wrappedSymbols.every((symbol) => poolMatrixTokens.has(symbol)); return { code: asset.code, name: asset.name, canonicalSymbols, wrappedSymbols, transportActive, canonicalDeployed: Boolean(manifestEntry?.status?.deployed), x402Ready: Boolean(manifestEntry?.status?.x402Ready), coveredByPoolMatrix, nextSteps: transportActive ? ['monitor_live_transport', 'deploy_public_pools'] : ['enable_bridge_controls', 'set_max_outstanding', 'promote_transport_overlay', 'deploy_public_pools'] }; }); const chainQueue = desiredChainIds.map((chainId) => { const chain = deployment.chains?.[String(chainId)] || {}; const matrix = poolMatrix.chains?.[String(chainId)] || {}; const cwTokens = Object.keys(chain.cwTokens || {}); const pmmPools = Array.isArray(chain.pmmPools) ? chain.pmmPools : []; const plannedWave1Pairs = (matrix.poolsFirst || []).filter((pair) => { return wave1WrappedSymbols.some((symbol) => normalizePair(pair).startsWith(`${symbol.toUpperCase()}/`)); }); const recordedWave1Pairs = plannedWave1Pairs.filter((pair) => pmmPools.some((entry) => poolEntryMatchesPair(entry, pair))); return { chainId, name: matrix.name || chain.name || `Chain ${chainId}`, hubStable: matrix.hubStable || null, bridgeAvailable: chain.bridgeAvailable === true, cwTokenCount: cwTokens.length, wave1WrappedCoverage: wave1WrappedSymbols.filter((symbol) => cwTokens.includes(symbol)).length, plannedWave1Pairs, recordedWave1Pairs, nextStep: cwTokens.length === 0 ? 'complete_cw_suite_then_deploy_pools' : recordedWave1Pairs.length === plannedWave1Pairs.length && plannedWave1Pairs.length > 0 ? 'verify_and_route' : 'deploy_first_tier_wave1_pools' }; }); const totalRecordedPublicPools = desiredChainIds.reduce((sum, chainId) => { const chain = deployment.chains?.[String(chainId)] || {}; const pmmPools = Array.isArray(chain.pmmPools) ? chain.pmmPools : []; return sum + pmmPools.length; }, 0); const protocolQueue = (protocolPlan.protocols || []).map((protocol) => { if (protocol.key === 'dodo_v3_d3mm') { return { key: protocol.key, name: protocol.name, role: protocol.role, deploymentStage: protocol.deploymentStage, activePublicPools: 0, currentState: 'pilot_live_chain138_only', activationDependsOn: protocol.activationDependsOn || [] }; } if (protocol.key === 'dodo_pmm') { return { key: protocol.key, name: protocol.name, role: protocol.role, deploymentStage: protocol.deploymentStage, activePublicPools: totalRecordedPublicPools, currentState: totalRecordedPublicPools > 0 ? 'partially_live_on_public_cw_mesh' : 'queued_not_live', activationDependsOn: protocol.activationDependsOn || [] }; } return { key: protocol.key, name: protocol.name, role: protocol.role, deploymentStage: protocol.deploymentStage, activePublicPools: 0, currentState: 'queued_not_live', activationDependsOn: protocol.activationDependsOn || [] }; }); const summary = { wave1Assets: assetQueue.length, wave1TransportActive: assetQueue.filter((item) => item.transportActive).length, wave1TransportPending: assetQueue.filter((item) => !item.transportActive).length, wave1WrappedSymbols: wave1WrappedSymbols.length, wave1WrappedSymbolsCoveredByPoolMatrix: wave1WrappedSymbols.length - poolMatrixMissingWave1.length, wave1WrappedSymbolsMissingFromPoolMatrix: poolMatrixMissingWave1.length, desiredPublicEvmTargets: chainQueue.length, chainsWithLoadedCwSuites: chainQueue.filter((item) => item.cwTokenCount > 0).length, chainsMissingCwSuites: chainQueue.filter((item) => item.cwTokenCount === 0).length, firstTierWave1PoolsPlanned: chainQueue.reduce((sum, item) => sum + item.plannedWave1Pairs.length, 0), firstTierWave1PoolsRecordedLive: chainQueue.reduce((sum, item) => sum + item.recordedWave1Pairs.length, 0), protocolsTracked: protocolQueue.length, protocolsLive: protocolQueue.filter((item) => item.activePublicPools > 0).length }; const blockers = []; if (poolMatrixMissingWave1.length > 0) { blockers.push(`Wave 1 wrapped symbols missing from pool-matrix: ${poolMatrixMissingWave1.join(', ')}.`); } const missingSuiteChains = chainQueue.filter((item) => item.cwTokenCount === 0); if (missingSuiteChains.length > 0) { blockers.push(`Desired public EVM targets still missing cW suites: ${missingSuiteChains.map((item) => item.name).join(', ')}.`); } const pendingWave1 = assetQueue.filter((item) => !item.transportActive); if (pendingWave1.length > 0) { blockers.push(`Wave 1 transport is still pending for: ${pendingWave1.map((item) => item.code).join(', ')}.`); } if (summary.firstTierWave1PoolsRecordedLive === 0) { blockers.push('No first-tier Wave 1 public cW pools are recorded live yet across the tracked public EVM mesh.'); } if (protocolQueue.every((item) => item.activePublicPools === 0)) { blockers.push('All tracked public protocols remain queued: Uniswap v3, DODO PMM, Balancer, Curve 3, and 1inch.'); } if (arbitrumHubBlocker.active) { blockers.push(`Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx ${arbitrumHubBlocker.failedTxHash} reverted before any bridge event was emitted.`); } const resolutionMatrix = [ { key: 'mainnet_arbitrum_hub_blocked', state: arbitrumHubBlocker.active ? 'open' : 'resolved', blocker: arbitrumHubBlocker.active ? `Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx ${arbitrumHubBlocker.failedTxHash} reverted from ${arbitrumHubBlocker.sourceBridge} before any bridge event was emitted.` : 'The Mainnet -> Arbitrum WETH9 hub leg is healthy.', targets: [ { fromChain: arbitrumHubBlocker.fromChain, viaChain: arbitrumHubBlocker.viaChain, toChain: arbitrumHubBlocker.toChain, currentPath: arbitrumHubBlocker.currentPath } ], resolution: [ 'Repair or replace the current Mainnet WETH9 fan-out bridge before treating Arbitrum as an available public bootstrap target.', 'Retest 138 -> Mainnet first-hop delivery, then rerun a smaller Mainnet -> Arbitrum send and require destination bridge events before promoting the route.', 'Keep Arbitrum marked blocked in the explorer and status surfaces until the hub leg emits and completes normally.' ], runbooks: [ 'docs/07-ccip/CROSS_NETWORK_FUNDING_BOOTSTRAP_STRATEGY.md', 'docs/07-ccip/CHAIN138_PUBLIC_CHAIN_UNLOAD_ROUTES.md', 'docs/00-meta/REQUIRED_FIXES_GAPS_AND_DEPLOYMENTS_LIST.md' ], exitCriteria: 'A fresh Mainnet -> Arbitrum WETH9 send emits bridge events and completes destination delivery successfully.' }, { key: 'missing_public_cw_suites', state: missingSuiteChains.length === 0 ? 'resolved' : 'open', blocker: missingSuiteChains.length === 0 ? 'All desired public EVM targets have cW suites.' : `Desired public EVM targets still missing cW suites: ${missingSuiteChains.map((item) => item.name).join(', ')}.`, targets: missingSuiteChains.map((item) => ({ chainId: item.chainId, name: item.name, nextStep: item.nextStep })), resolution: [ 'Deploy the full cW core suite on each missing destination chain using the existing CW deploy-and-wire flow.', 'Grant bridge mint/burn roles and mark the corridor live in cross-chain-pmm-lps/config/deployment-status.json.', 'Update public token lists / explorer config, then rerun check-cw-evm-deployment-mesh.sh and check-cw-public-pool-status.sh.' ], runbooks: [ 'docs/07-ccip/CW_DEPLOY_AND_WIRE_RUNBOOK.md', 'docs/03-deployment/PHASE_C_CW_AND_EDGE_POOLS_RUNBOOK.md', 'scripts/deployment/run-cw-remaining-steps.sh', 'scripts/verify/check-cw-evm-deployment-mesh.sh' ], exitCriteria: missingSuiteChains.length === 0 ? 'All desired public EVM targets report non-zero cW suites and bridgeAvailable=true in deployment-status.json.' : `${missingSuiteChains.map((item) => item.name).join(', ')} report non-zero cW suites and become bridgeAvailable in deployment-status.json.` }, { key: 'wave1_transport_pending', state: pendingWave1.length === 0 ? 'resolved' : 'open', blocker: pendingWave1.length === 0 ? 'Wave 1 transport is fully active.' : `Wave 1 transport is still pending for: ${pendingWave1.map((item) => item.code).join(', ')}.`, targets: pendingWave1.map((item) => ({ code: item.code, canonicalSymbols: item.canonicalSymbols, wrappedSymbols: item.wrappedSymbols })), resolution: [ 'Enable bridge controls and supervision policy for each Wave 1 canonical asset on Chain 138.', 'Set max-outstanding / capacity controls, then promote the canonical symbols into config/gru-transport-active.json.', 'Verify the overlay promotion with check-gru-global-priority-rollout.sh and check-gru-v2-chain138-readiness.sh before attaching public liquidity.' ], runbooks: [ 'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md', 'docs/04-configuration/GRU_TRANSPORT_ACTIVE_JSON.md', 'scripts/verify/check-gru-global-priority-rollout.sh', 'scripts/verify/check-gru-v2-chain138-readiness.sh' ], exitCriteria: 'Wave 1 transport pending count reaches zero and the overlay reports the seven non-USD assets as live_transport.' }, { key: 'first_tier_public_pools_not_live', state: summary.firstTierWave1PoolsRecordedLive > 0 ? 'in_progress' : 'open', blocker: summary.firstTierWave1PoolsRecordedLive > 0 ? 'Some first-tier Wave 1 public cW pools are live, but the rollout is incomplete.' : 'No first-tier Wave 1 public cW pools are recorded live yet across the tracked public EVM mesh.', targets: chainQueue.map((item) => ({ chainId: item.chainId, name: item.name, hubStable: item.hubStable, plannedWave1Pairs: item.plannedWave1Pairs.length, recordedWave1Pairs: item.recordedWave1Pairs.length })), resolution: [ 'Deploy the first-tier cW/hub-stable pairs from pool-matrix.json on every chain with a loaded cW suite.', 'Seed the new pools with initial liquidity and record the resulting pool addresses in cross-chain-pmm-lps/config/deployment-status.json.', 'Use check-cw-public-pool-status.sh to verify the mesh is no longer empty before surfacing the venues publicly.' ], runbooks: [ 'docs/03-deployment/SINGLE_SIDED_LPS_PUBLIC_NETWORKS_RUNBOOK.md', 'docs/03-deployment/PMM_FULL_MESH_AND_PUBLIC_SINGLE_SIDED_PLAN.md', 'cross-chain-pmm-lps/config/pool-matrix.json', 'scripts/verify/check-cw-public-pool-status.sh' ], exitCriteria: 'First-tier Wave 1 pools are recorded live in deployment-status.json and check-cw-public-pool-status.sh reports non-zero pool coverage.' }, { key: 'public_protocols_queued', state: protocolQueue.every((item) => item.activePublicPools === 0) ? 'open' : 'in_progress', blocker: protocolQueue.every((item) => item.activePublicPools === 0) ? 'All tracked public protocols remain queued: Uniswap v3, DODO PMM, Balancer, Curve 3, and 1inch.' : 'Some tracked public protocols have begun activation, but the full protocol stack is not live yet.', targets: protocolQueue.map((item) => ({ key: item.key, name: item.name, deploymentStage: item.deploymentStage, activationDependsOn: item.activationDependsOn })), resolution: [ 'Stage 1: activate Uniswap v3 and DODO PMM once first-tier cW pools exist on the public mesh.', 'Stage 2: activate Balancer and Curve 3 only after first-tier stable liquidity is already live.', 'Stage 3: expose 1inch after the underlying pools, routing/indexer visibility, and public provider-capability wiring are in place.' ], runbooks: [ 'config/gru-v2-public-protocol-rollout-plan.json', 'docs/11-references/GRU_V2_PUBLIC_PROTOCOL_DEPLOYMENT_STATUS.md', 'scripts/verify/check-gru-v2-public-protocols.sh' ], exitCriteria: 'The public protocol status surface reports non-zero active cW pools for the staged venues.' }, { key: 'global_priority_backlog', state: rolloutBacklogAssets === 0 ? 'resolved' : 'open', blocker: rolloutBacklogAssets === 0 ? 'No ranked GRU backlog assets remain outside the live manifest.' : `The ranked GRU global rollout still has ${rolloutBacklogAssets} backlog assets outside the live manifest.`, targets: [ { backlogAssets: rolloutBacklogAssets } ], resolution: [ 'Complete Wave 1 transport and first-tier public liquidity before promoting the remaining ranked assets.', 'For each backlog asset, add canonical + wrapped symbols to the manifest/rollout plan, deploy contracts, and extend the public pool matrix.', 'Promote each new asset through the same transport and public-liquidity gates used for Wave 1.' ], runbooks: [ 'config/gru-global-priority-currency-rollout.json', 'config/gru-iso4217-currency-manifest.json', 'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md', 'scripts/verify/check-gru-global-priority-rollout.sh' ], exitCriteria: 'Backlog assets count reaches zero in check-gru-global-priority-rollout.sh.' }, { key: 'solana_non_evm_program', state: ((rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).length || 0) === 0 ? 'resolved' : 'planned', blocker: ((rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).length || 0) === 0 ? 'No desired non-EVM GRU targets remain.' : `Desired non-EVM GRU targets remain planned / relay-dependent: ${(rollout.desiredDestinationNetworks.nonEvmRelayPrograms || []).map((item) => item.identifier).join(', ')}.`, targets: (rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).map((item) => ({ identifier: item.identifier, label: item.label || item.identifier })), resolution: [ 'Define the destination-chain token/program model first: SPL or wrapped-account representation, authority model, and relay custody surface.', 'Implement the relay/program path and only then promote Solana from desired-target status into the active transport inventory.', 'Add dedicated verifier coverage before marking Solana live anywhere in the explorer or status docs.' ], runbooks: [ 'docs/04-configuration/ADDITIONAL_PATHS_AND_EXTENSIONS.md', 'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md' ], exitCriteria: 'Solana has a real relay/program surface, a verifier, and is no longer only listed as a desired non-EVM target.' } ]; const report = { generatedAt: new Date().toISOString(), summary, assetQueue, chainQueue, protocolQueue, blockers, resolutionMatrix, notes: [ 'This queue is an operator/deployment planning surface. It does not mark queued pools or transports as live.', 'Chain 138 canonical venues remain a separate live surface from the public cW mesh.' ] }; if (writeExplorerConfig) { fs.writeFileSync(explorerConfigPath, `${JSON.stringify(report, null, 2)}\n`); } if (outputJson) { console.log(JSON.stringify(report, null, 2)); process.exit(0); } console.log('=== GRU V2 Deployment Queue ==='); console.log(`Wave 1 assets: ${summary.wave1Assets}`); console.log(`Wave 1 transport active: ${summary.wave1TransportActive}`); console.log(`Wave 1 transport pending: ${summary.wave1TransportPending}`); console.log(`Wave 1 wrapped symbols covered by pool-matrix: ${summary.wave1WrappedSymbolsCoveredByPoolMatrix}/${summary.wave1WrappedSymbols}`); console.log(`Desired public EVM targets: ${summary.desiredPublicEvmTargets}`); console.log(`Chains with loaded cW suites: ${summary.chainsWithLoadedCwSuites}`); console.log(`Chains missing cW suites: ${summary.chainsMissingCwSuites}`); console.log(`First-tier Wave 1 pools planned: ${summary.firstTierWave1PoolsPlanned}`); console.log(`First-tier Wave 1 pools recorded live: ${summary.firstTierWave1PoolsRecordedLive}`); console.log(`Tracked protocols: ${summary.protocolsTracked}`); console.log(''); console.log('Wave 1 asset queue:'); for (const asset of assetQueue) { console.log(`- ${asset.code}: transport=${asset.transportActive ? 'live' : 'pending'}; pool-matrix=${asset.coveredByPoolMatrix ? 'covered' : 'missing'}; next=${asset.nextSteps.join(',')}`); } console.log(''); console.log('Per-chain queue:'); for (const chain of chainQueue) { console.log(`- ${chain.chainId} ${chain.name}: hub=${chain.hubStable || 'n/a'}; cw=${chain.cwTokenCount}; plannedWave1Pairs=${chain.plannedWave1Pairs.length}; liveWave1Pairs=${chain.recordedWave1Pairs.length}; next=${chain.nextStep}`); } console.log(''); console.log('Protocol queue:'); for (const protocol of protocolQueue) { console.log(`- ${protocol.name}: ${protocol.currentState}; stage=${protocol.deploymentStage}`); } if (blockers.length > 0) { console.log(''); console.log('Active blockers:'); for (const blocker of blockers) { console.log(`- ${blocker}`); } console.log(''); console.log('Resolution paths:'); for (const entry of resolutionMatrix) { if (entry.state === 'resolved') continue; console.log(`- ${entry.key}: ${entry.exitCriteria}`); } } if (writeExplorerConfig) { console.log(''); console.log(`Wrote: ${explorerConfigPath}`); } NODE