#!/usr/bin/env bash # Summarize the GRU v2 public-network rollout posture across the public EVM cW* # mesh, Wave 1 transport activation, and public protocol liquidity. # # Usage: # bash scripts/verify/check-gru-v2-public-protocols.sh # bash scripts/verify/check-gru-v2-public-protocols.sh --json # bash scripts/verify/check-gru-v2-public-protocols.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_PUBLIC_DEPLOYMENT_STATUS.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 mapping = readJson('config/token-mapping-multichain.json'); const poolMatrix = readJson('cross-chain-pmm-lps/config/pool-matrix.json'); const routingRegistry = readJson('config/routing-registry.json'); const desiredChainIds = rollout.desiredDestinationNetworks?.evmPublicCwMeshChainIds || []; const chainNames = mapping.chainNames || {}; const manifestByCode = new Map((manifest.currencies || []).map((item) => [item.code, item])); const transportBySymbol = new Map((transport.enabledCanonicalTokens || []).map((item) => [item.symbol, item])); const coreCwSymbols = [ 'cWUSDT', 'cWUSDC', 'cWEURC', 'cWEURT', 'cWGBPC', 'cWGBPT', 'cWAUDC', 'cWJPYC', 'cWCHFC', 'cWCADC', 'cWXAUC', 'cWXAUT' ]; const publicProtocols = [ { key: 'uniswap_v3', name: 'Uniswap v3' }, { key: 'balancer', name: 'Balancer' }, { key: 'curve_3', name: 'Curve 3' }, { key: 'dodo_pmm', name: 'DODO PMM' }, { key: 'one_inch', name: '1inch' } ]; const desiredChainRows = desiredChainIds.map((chainId) => { const chain = deployment.chains?.[String(chainId)] || {}; const cwTokens = Object.keys(chain.cwTokens || {}); const pmmPools = Array.isArray(chain.pmmPools) ? chain.pmmPools : []; return { chainId, name: chain.name || chainNames[String(chainId)] || `Chain ${chainId}`, cwTokenCount: cwTokens.length, hasFullCoreSuite: coreCwSymbols.every((symbol) => cwTokens.includes(symbol)), bridgeAvailable: chain.bridgeAvailable === true, pmmPoolCount: pmmPools.length }; }); const desiredButNotLoaded = desiredChainRows.filter((row) => row.cwTokenCount === 0); const loadedChains = desiredChainRows.filter((row) => row.cwTokenCount > 0); const fullCoreSuiteChains = desiredChainRows.filter((row) => row.hasFullCoreSuite); const chainsWithAnyPools = desiredChainRows.filter((row) => row.pmmPoolCount > 0); const totalRecordedPublicPools = desiredChainRows.reduce((sum, row) => sum + row.pmmPoolCount, 0); 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 currencyState(code) { const item = manifestByCode.get(code); return { manifestPresent: Boolean(item), deployed: Boolean(item?.status?.deployed), transportActive: Boolean(item?.status?.transportActive), x402Ready: Boolean(item?.status?.x402Ready) }; } const allAssetResults = (rollout.assets || []).map((asset) => { const state = currencyState(asset.code); const transportSymbols = (asset.tokenForms || []).map((item) => item.canonicalSymbol); const enabledByOverlay = transportSymbols.some((symbol) => transportBySymbol.has(symbol)); return { code: asset.code, name: asset.name, wave: asset.wave, manifestPresent: state.manifestPresent, deployed: state.deployed, transportActive: state.transportActive && enabledByOverlay, x402Ready: state.x402Ready, canonicalSymbols: (asset.tokenForms || []).map((item) => item.canonicalSymbol), wrappedSymbols: (asset.tokenForms || []).map((item) => item.wrappedSymbol) }; }); const wave1Results = allAssetResults .filter((asset) => asset.wave === 'wave1') .map((asset) => ({ ...asset, currentState: asset.transportActive ? 'live_transport' : asset.deployed ? 'canonical_only' : asset.manifestPresent ? 'manifest_only' : 'backlog', nextStep: asset.transportActive ? 'monitor_and_scale' : asset.deployed ? 'activate_transport_and_attach_public_liquidity' : asset.manifestPresent ? 'finish_canonical_deployment' : 'add_to_manifest' })); const wave1WrappedSymbols = [...new Set(wave1Results.flatMap((asset) => asset.wrappedSymbols))]; const poolMatrixCwTokens = new Set(poolMatrix.cwTokens || []); const wave1WrappedSymbolsMissingFromPoolMatrix = wave1WrappedSymbols.filter((symbol) => !poolMatrixCwTokens.has(symbol)); const rolloutSummary = { liveTransportAssets: allAssetResults.filter((asset) => asset.transportActive).length, canonicalOnlyAssets: allAssetResults.filter((asset) => asset.deployed && !asset.transportActive).length, backlogAssets: allAssetResults.filter((asset) => !asset.manifestPresent).length, wave1LiveTransport: wave1Results.filter((asset) => asset.currentState === 'live_transport').length, wave1CanonicalOnly: wave1Results.filter((asset) => asset.currentState === 'canonical_only').length, wave1WrappedSymbols: wave1WrappedSymbols.length, wave1WrappedSymbolsCoveredByPoolMatrix: wave1WrappedSymbols.length - wave1WrappedSymbolsMissingFromPoolMatrix.length }; const protocolResults = publicProtocols.map((protocol) => { if (protocol.key === 'dodo_pmm') { return { key: protocol.key, name: protocol.name, activePublicCwPools: totalRecordedPublicPools, destinationChainsWithPools: chainsWithAnyPools.length, status: totalRecordedPublicPools > 0 ? 'partial_live_on_public_cw_mesh' : 'not_deployed_on_public_cw_mesh', notes: totalRecordedPublicPools > 0 ? 'deployment-status.json now records live public-chain cW* DODO PMM pools on Mainnet, including recorded non-USD Wave 1 rows, and the recorded Mainnet pools now have bidirectional live execution proof. The broader public cW mesh is still partial.' : 'cross-chain-pmm-lps/config/deployment-status.json still records no public-chain cW* pools, so no live DODO PMM cW venue can be asserted.' }; } return { key: protocol.key, name: protocol.name, activePublicCwPools: 0, destinationChainsWithPools: 0, status: 'not_deployed_on_public_cw_mesh', notes: 'No live public-chain cW* venue is recorded for this protocol in deployment-status.json yet.' }; }); const blockers = []; if (desiredButNotLoaded.length > 0) { blockers.push(`Desired public EVM targets still lack cW token suites: ${desiredButNotLoaded.map((row) => row.name).join(', ')}.`); } if (rolloutSummary.wave1CanonicalOnly > 0) { blockers.push(`Wave 1 GRU assets are still canonical-only on Chain 138: ${wave1Results.filter((asset) => asset.currentState === 'canonical_only').map((asset) => asset.code).join(', ')}.`); } if (wave1WrappedSymbolsMissingFromPoolMatrix.length > 0) { blockers.push(`Wave 1 wrapped symbols are still missing from the public pool matrix: ${wave1WrappedSymbolsMissingFromPoolMatrix.join(', ')}.`); } if (chainsWithAnyPools.length === 0) { blockers.push('Public cW* liquidity is still undeployed across Uniswap v3, Balancer, Curve 3, DODO PMM, and 1inch on the tracked public-network mesh.'); } if (chainsWithAnyPools.length > 0 && protocolResults.some((item) => item.activePublicCwPools > 0) && protocolResults.some((item) => item.activePublicCwPools === 0)) { blockers.push('Public cW* protocol rollout is now partial: DODO PMM has recorded pools, while Uniswap v3, Balancer, Curve 3, and 1inch remain not live on the public cW mesh.'); } if (rolloutSummary.backlogAssets > 0) { blockers.push(`The ranked GRU global rollout still has ${rolloutSummary.backlogAssets} backlog assets outside the live manifest.`); } if ((rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).length > 0) { blockers.push(`Desired non-EVM GRU targets remain planned / relay-dependent: ${(rollout.desiredDestinationNetworks.nonEvmRelayPrograms || []).map((item) => item.identifier).join(', ')}.`); } if (arbitrumHubBlocker.active) { blockers.push(`Arbitrum public-network bootstrap remains blocked on the current Mainnet hub leg: tx ${arbitrumHubBlocker.failedTxHash} reverted from ${arbitrumHubBlocker.sourceBridge} before any bridge event was emitted.`); } const report = { generatedAt: new Date().toISOString(), canonicalChainId: 138, summary: { desiredPublicEvmTargets: desiredChainRows.length, loadedPublicEvmChains: loadedChains.length, loadedPublicEvmFullCoreSuite: fullCoreSuiteChains.length, desiredButNotLoaded: desiredButNotLoaded.length, publicProtocolsTracked: protocolResults.length, publicProtocolsWithActiveCwPools: protocolResults.filter((item) => item.activePublicCwPools > 0).length, chainsWithAnyRecordedPublicCwPools: chainsWithAnyPools.length, liveTransportAssets: rolloutSummary.liveTransportAssets, wave1CanonicalOnly: rolloutSummary.wave1CanonicalOnly, backlogAssets: rolloutSummary.backlogAssets }, publicEvmMesh: { coreCwSuite: coreCwSymbols, desiredChains: desiredChainRows, desiredButNotLoaded: desiredButNotLoaded.map((row) => ({ chainId: row.chainId, name: row.name })), wave1PoolMatrixCoverage: { totalWrappedSymbols: wave1WrappedSymbols.length, coveredSymbols: rolloutSummary.wave1WrappedSymbolsCoveredByPoolMatrix, missingSymbols: wave1WrappedSymbolsMissingFromPoolMatrix }, note: desiredButNotLoaded.length > 0 ? `The public EVM cW token mesh is complete on the currently loaded ${loadedChains.length}-chain set, but ${desiredButNotLoaded.map((row) => row.name).join(', ')} ${desiredButNotLoaded.length === 1 ? 'remains a desired target without a cW suite' : 'remain desired targets without cW suites'} in deployment-status.json.` : `The public EVM cW token mesh is complete across all ${loadedChains.length} desired public EVM targets recorded in deployment-status.json.` }, transport: { liveTransportAssets: allAssetResults.filter((asset) => asset.transportActive).map((asset) => ({ code: asset.code, name: asset.name })), wave1: wave1Results, note: 'USD is the only live transport asset today. Wave 1 non-USD assets are deployed canonically on Chain 138 but are not yet promoted into the active transport overlay.' }, protocols: { publicCwMesh: protocolResults, chain138CanonicalVenues: { note: 'Chain 138 canonical routing is a separate surface: DODO PMM plus upstream-native Uniswap v3 and the funded pilot-compatible Balancer, Curve 3, and 1inch venues are live there.', liveProtocols: ['DODO PMM', 'Uniswap v3', 'Balancer', 'Curve 3', '1inch'] } }, bridgeRouteHealth: { arbitrumHubBlocker }, explorer: { tokenListApi: 'https://explorer.d-bis.org/api/config/token-list', staticStatusPath: 'https://explorer.d-bis.org/config/GRU_V2_PUBLIC_DEPLOYMENT_STATUS.json' }, blockers }; 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 Public-Protocol Rollout Status ==='); console.log(`Desired public EVM targets: ${report.summary.desiredPublicEvmTargets}`); console.log(`Loaded public EVM chains: ${report.summary.loadedPublicEvmChains}`); console.log(`Loaded chains with full core cW suite: ${report.summary.loadedPublicEvmFullCoreSuite}`); console.log(`Desired targets still unloaded: ${report.summary.desiredButNotLoaded}`); console.log(`Live transport assets: ${report.summary.liveTransportAssets}`); console.log(`Wave 1 canonical-only assets: ${report.summary.wave1CanonicalOnly}`); console.log(`Wave 1 wrapped symbols covered by pool-matrix: ${report.publicEvmMesh.wave1PoolMatrixCoverage.coveredSymbols}/${report.publicEvmMesh.wave1PoolMatrixCoverage.totalWrappedSymbols}`); console.log(`Backlog assets: ${report.summary.backlogAssets}`); console.log(`Tracked public protocols: ${report.summary.publicProtocolsTracked}`); console.log(`Protocols with active public cW pools: ${report.summary.publicProtocolsWithActiveCwPools}`); console.log(`Chains with any recorded public cW pools: ${report.summary.chainsWithAnyRecordedPublicCwPools}`); console.log(''); console.log('Wave 1:'); for (const asset of wave1Results) { console.log(`- ${asset.code} (${asset.name}) -> ${asset.currentState}; next: ${asset.nextStep}`); } console.log(''); console.log('Public protocol surface:'); for (const protocol of protocolResults) { console.log(`- ${protocol.name}: ${protocol.status}`); } if (blockers.length > 0) { console.log(''); console.log('Active blockers:'); for (const blocker of blockers) { console.log(`- ${blocker}`); } } if (writeExplorerConfig) { console.log(''); console.log(`Wrote: ${explorerConfigPath}`); } NODE