diff --git a/apps/dashboard/public/data/flowchain-l1-explorer-fallback.json b/apps/dashboard/public/data/flowchain-l1-explorer-fallback.json new file mode 100644 index 00000000..1db8cd4a --- /dev/null +++ b/apps/dashboard/public/data/flowchain-l1-explorer-fallback.json @@ -0,0 +1,377 @@ +{ + "schema": "flowmemory.dashboard.explorer_fallback.v0", + "generatedAt": "2026-05-14T00:00:00.000Z", + "source": "fixture-fallback", + "provenance": { + "subsystem": "dashboard", + "origin": "fixture-fallback", + "path": "fixtures/dashboard/flowchain-l1-explorer-fallback.json", + "note": "Deterministic browser-safe fallback used only when live local runtime maps are missing." + }, + "pilotReadiness": { + "schema": "flowmemory.dashboard.explorer_pilot_readiness.v0", + "state": "degraded", + "baseChainId": 8453, + "lockboxAddress": "0x1111111111111111111111111111111111111111", + "perDepositCapUsd": 25, + "totalPilotCapUsd": 25, + "pauseStatus": "unpaused", + "emergencyStatus": "standby", + "latestObservationBlockRange": { + "fromBlock": "45955500", + "toBlock": "45955540" + }, + "confirmationDepth": 12, + "requiredEnvNames": [ + "FLOWCHAIN_BASE8453_RPC_URL", + "FLOWCHAIN_BASE8453_LOCKBOX_ADDRESS" + ], + "broadPublicReadiness": false, + "productionReady": false, + "localOnly": true + }, + "objects": { + "tokens": { + "token:flowchain-pilot-ltu": { + "schema": "flowmemory.explorer.token.v0", + "tokenId": "token:flowchain-pilot-ltu", + "symbol": "FCLT", + "name": "FlowChain Local Test Credit", + "decimals": 6, + "totalSupply": "25000000", + "owner": "operator:local-demo", + "issuer": "operator:local-demo", + "launchTxId": "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7", + "status": "fixture_fallback_visible", + "provenance": "fixture-fallback", + "noValue": true, + "localOnly": true + } + }, + "tokenBalances": { + "token-balance:pilot-recipient:fclt": { + "schema": "flowmemory.explorer.token_balance.v0", + "balanceId": "token-balance:pilot-recipient:fclt", + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "tokenId": "token:flowchain-pilot-ltu", + "amount": "20000000", + "status": "fixture_fallback_visible", + "updatedAtBlock": "2", + "provenance": "fixture-fallback", + "noValue": true, + "localOnly": true + } + }, + "tokenTransfers": { + "token-transfer:pilot-credit-to-swap": { + "schema": "flowmemory.explorer.token_transfer.v0", + "transferId": "token-transfer:pilot-credit-to-swap", + "txId": "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", + "tokenId": "token:flowchain-pilot-ltu", + "fromAccount": "bridge-credit:operator", + "toAccount": "0x5555555555555555555555555555555555555555555555555555555555555555", + "amount": "20000000", + "status": "fixture_fallback_visible", + "blockNumber": "2", + "provenance": "fixture-fallback", + "localOnly": true + } + }, + "pools": { + "pool:fclt-local-unit": { + "schema": "flowmemory.explorer.dex_pool.v0", + "poolId": "pool:fclt-local-unit", + "token0": "local-test-unit", + "token1": "token:flowchain-pilot-ltu", + "reserve0": "12500000", + "reserve1": "20000000", + "lpSupply": "5000000", + "status": "fixture_fallback_visible", + "createdTxId": "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "provenance": "fixture-fallback", + "noValue": true, + "localOnly": true + } + }, + "lpPositions": { + "lp:operator:fclt-local-unit": { + "schema": "flowmemory.explorer.lp_position.v0", + "positionId": "lp:operator:fclt-local-unit", + "accountId": "operator:local-demo", + "poolId": "pool:fclt-local-unit", + "liquidity": "5000000", + "amount0": "12500000", + "amount1": "20000000", + "status": "fixture_fallback_visible", + "updatedAtBlock": "2", + "provenance": "fixture-fallback", + "noValue": true, + "localOnly": true + } + }, + "liquidityEvents": { + "liquidity:operator:add:fclt-local-unit": { + "schema": "flowmemory.explorer.liquidity_event.v0", + "liquidityEventId": "liquidity:operator:add:fclt-local-unit", + "txId": "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "accountId": "operator:local-demo", + "poolId": "pool:fclt-local-unit", + "action": "add", + "amount0": "12500000", + "amount1": "20000000", + "status": "fixture_fallback_visible", + "provenance": "fixture-fallback", + "localOnly": true + } + }, + "swaps": { + "swap:pilot:fclt-local-unit": { + "schema": "flowmemory.explorer.swap.v0", + "swapId": "swap:pilot:fclt-local-unit", + "txId": "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad", + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "poolId": "pool:fclt-local-unit", + "tokenIn": "token:flowchain-pilot-ltu", + "tokenOut": "local-test-unit", + "amountIn": "2500000", + "amountOut": "1512000", + "status": "fixture_fallback_visible", + "provenance": "fixture-fallback", + "noValue": true, + "localOnly": true + } + }, + "withdrawals": { + "withdrawal:pilot:release-request": { + "schema": "flowmemory.explorer.withdrawal.v0", + "withdrawalId": "withdrawal:pilot:release-request", + "withdrawalIntentId": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751", + "creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "accountId": "0x5555555555555555555555555555555555555555555555555555555555555555", + "amount": "20000000", + "token": "token:flowchain-pilot-ltu", + "status": "requested", + "provenance": "fixture-fallback", + "localOnly": true + } + } + }, + "bridge": { + "observations": [ + { + "schema": "flowmemory.bridge_deposit_observation.v0", + "observationId": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c", + "replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09", + "observedAt": "2026-05-14T00:00:00.000Z", + "mode": "base-mainnet-canary", + "productionReady": false, + "localOnly": true, + "deposit": { + "schema": "flowmemory.bridge_deposit.v0", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "sourceChainId": 8453, + "sourceContract": "0x1111111111111111111111111111111111111111", + "lockboxAddress": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0, + "blockNumber": "45955512", + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "sender": "0x4444444444444444444444444444444444444444", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "nonce": "1", + "status": "observed" + }, + "guardrails": { + "explicitChainId": true, + "explicitContract": true, + "explicitBlockRange": true, + "noSecrets": true, + "maxUsd": 20 + } + }, + { + "schema": "flowmemory.bridge_deposit_observation.v0", + "observationId": "0x1430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223d", + "replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09", + "observedAt": "2026-05-14T00:00:05.000Z", + "mode": "base-mainnet-canary", + "productionReady": false, + "localOnly": true, + "deposit": { + "schema": "flowmemory.bridge_deposit.v0", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "sourceChainId": 8453, + "sourceContract": "0x1111111111111111111111111111111111111111", + "lockboxAddress": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0, + "blockNumber": "45955512", + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "sender": "0x4444444444444444444444444444444444444444", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "nonce": "1", + "status": "duplicate_replay_rejected" + }, + "guardrails": { + "explicitChainId": true, + "explicitContract": true, + "explicitBlockRange": true, + "noSecrets": true, + "maxUsd": 20 + } + } + ], + "credits": [ + { + "schema": "flowmemory.bridge_credit.v0", + "creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6", + "observationId": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09", + "source": { + "chainId": 8453, + "contract": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0 + }, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "status": "applied", + "appliedAt": "2026-05-14T00:00:01.000Z", + "localOnly": true, + "productionReady": false + }, + { + "schema": "flowmemory.bridge_credit.v0", + "creditId": "0x9f3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a9", + "observationId": "0x1430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223d", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09", + "source": { + "chainId": 8453, + "contract": "0x1111111111111111111111111111111111111111", + "txHash": "0x2222222222222222222222222222222222222222222222222222222222222222", + "logIndex": 0 + }, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555", + "status": "rejected", + "rejectionReason": "duplicate_replay_key", + "localOnly": true, + "productionReady": false + } + ], + "withdrawalIntents": [ + { + "schema": "flowmemory.bridge_withdrawal_intent.v0", + "withdrawalIntentId": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751", + "creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "sourceChainId": 8453, + "destinationChainId": 8453, + "token": "0x3333333333333333333333333333333333333333", + "amount": "20000000", + "flowchainAccount": "0x5555555555555555555555555555555555555555555555555555555555555555", + "baseRecipient": "0x4444444444444444444444444444444444444444", + "status": "requested", + "requestedAt": "2026-05-14T00:00:02.000Z", + "broadcast": false, + "releasePolicy": "operator_release_evidence_required", + "productionReady": false, + "localOnly": true + } + ], + "releaseEvidence": [ + { + "schema": "flowmemory.bridge_release_evidence.v0", + "releaseEvidenceId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb278", + "withdrawalIntentId": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751", + "creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6", + "depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269", + "status": "recorded", + "releaseTxHash": "0x8888888888888888888888888888888888888888888888888888888888888888", + "recordedAt": "2026-05-14T00:00:03.000Z", + "operatorNote": "Fixture fallback proves the explorer renders release evidence without storing operator secrets.", + "localOnly": true, + "productionReady": false + } + ], + "replayProtection": { + "strategy": "source-chain-contract-tx-log-deposit", + "replayKeys": [ + "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09" + ], + "duplicateReplayKeys": [ + "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09" + ] + } + }, + "network": { + "node": { + "status": "degraded", + "syncStatus": "fixture-fallback", + "latestHeight": "2", + "finalizedHeight": "1", + "stateRoot": "0x3074ef2e5311d94e8f9a2660a6cc016c7b7f9a08c56ee07f9e841c1489726e68", + "peerCount": 1 + }, + "peers": { + "peer:local-single-node": { + "peerId": "peer:local-single-node", + "address": "127.0.0.1", + "status": "self", + "height": "2", + "provenance": "fixture-fallback" + } + } + }, + "errors": { + "runtime-offline": { + "errorId": "runtime-offline", + "subsystem": "runtime", + "state": "degraded", + "summary": "Long-running runtime process is not required for fallback inspection.", + "recoveryCommand": "npm run flowchain:start" + }, + "api-offline": { + "errorId": "api-offline", + "subsystem": "control-plane", + "state": "degraded", + "summary": "Dashboard uses deterministic fallback when the local API is offline.", + "recoveryCommand": "npm run control-plane:serve" + }, + "storage-unavailable": { + "errorId": "storage-unavailable", + "subsystem": "storage", + "state": "degraded", + "summary": "Local file-backed sources are used when durable runtime storage is absent.", + "recoveryCommand": "npm run flowchain:export" + }, + "bridge-relayer-offline": { + "errorId": "bridge-relayer-offline", + "subsystem": "bridge", + "state": "degraded", + "summary": "Bridge rows are fixture fallback until the relayer exports a fresh observation.", + "recoveryCommand": "npm run bridge:observe -- --mode base-mainnet-canary --acknowledge-real-funds --max-usd 25" + }, + "wrong-chain-id": { + "errorId": "wrong-chain-id", + "subsystem": "bridge", + "state": "blocked", + "summary": "Pilot observation must be on Base chain ID 8453.", + "recoveryCommand": "npm run control-plane:smoke" + }, + "duplicate-event-rejected": { + "errorId": "duplicate-event-rejected", + "subsystem": "bridge", + "state": "verified", + "summary": "Duplicate replay key is visible and rejected in the fallback evidence set.", + "recoveryCommand": "npm run flowchain:real-value-pilot:e2e" + } + } +} diff --git a/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json index 2ecd1722..7d49bdb2 100644 --- a/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json +++ b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json @@ -31,19 +31,25 @@ }, "balanceTransfers": {}, "baseAnchors": { - "0xf57ab7d2c1459c03cf01bfddd56b046be685d8eaa4597e6bb54b5015aeaf003f": { + "0x6908eac7dbf828ab8d295d2ae86f0b0db9295a27b1fe4999c26a3ed97244ef8d": { "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", - "anchorId": "0xf57ab7d2c1459c03cf01bfddd56b046be685d8eaa4597e6bb54b5015aeaf003f", + "anchorId": "0x6908eac7dbf828ab8d295d2ae86f0b0db9295a27b1fe4999c26a3ed97244ef8d", "appchainChainId": "flowmemory-local-devnet-v0", "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", "balanceTransferRoot": "0x9b6e249f769a93bc9f34a90156e028d1a830badcd8ccdc5b1487d512cdbf0a6d", "blockRangeEnd": 1, "blockRangeStart": 1, + "bridgeAccountMappingRoot": "0x46285ec5782d3e6cc1e557fd5dd779e979d679e5cc723832cb189d27a5c552c8", + "bridgeAssetMappingRoot": "0xbd283f6190cba76038f7364e207557dd20f7883d78e5f89a96579af5455a89aa", + "bridgeCreditReceiptRoot": "0x0a86ce98161cb40ca4c7719468c0ed8f122677d12641c13305277094b8f3a509", + "bridgeCreditRoot": "0x613ab06f110cc7a5eb06a0f44b7a0f04b0b3cfb0556d30d42842f33563596248", + "bridgeEventReceiptIndexRoot": "0xcaad305c45f57c516d0fdfdef15c5d88b399b976df173f005e9d7dc30fbdbdb2", + "bridgeReplayIndexRoot": "0x9344f8651651d2d5042d2853205154a5777cd57aa9e892e2ac41b9b9fe04c00f", "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", "dexPoolRoot": "0x0e5f034494a2deb6a4f20c04f01e678795c587df4869ad3f189f107fcc447dea", "faucetRecordRoot": "0x2277503a52fab3f9e49b40debfb7d641abee75cf268aa56da403fdcf4fad6cee", - "finalityReceiptRoot": "0xdf352e20fd1ddfd2855202335e03cfec21d87e99bf8717d161fe8648998e16cf", + "finalityReceiptRoot": "0x1c8536e507c5ba2242a5875c9537043645828825286ca256cc1171f1eb960c3f", "finalityStatus": "local-placeholder", "liquidityReceiptRoot": "0x98bf15cc6859994038744612e125236b1f777895a051c41702c2004134327738", "localTestUnitBalanceRoot": "0x167041ef195b5dde2d2cade6ecb26c9a0a596e9ed21ff7bfb02d33c9d2be8d15", @@ -53,7 +59,7 @@ "operatorKeyReferenceRoot": "0x8457aa3ed0f4238834a8f3925f25ccca805828d8427c3ef67590a45659b22a40", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", - "stateRoot": "0x8c7c1e7a078b60a809d17a51c44e275059afb8d7535769430c3fc9e9320c7e23", + "stateRoot": "0x7e50021bc0dd2537f9ecfd7973f9799b6ffdffcfd427e3bb059f4dfd1253b947", "swapReceiptRoot": "0xc9f3ee93962f36ed10421ec9ad736079a3b13ef6504336495af243b718cefee1", "tokenBalanceRoot": "0xbaf3b150fe41a0f3a2d9fe3dd9a664f9c5934bfef37218d9c3bf1c682be5f8c6", "tokenDefinitionRoot": "0xbbbad9681e8756403940e4333111706a4fcee1f30534ba14deea9ba148056be0", @@ -64,6 +70,12 @@ } }, "blockHeight": 2, + "bridgeAccountMappings": {}, + "bridgeAssetMappings": {}, + "bridgeCreditReceipts": {}, + "bridgeCredits": {}, + "bridgeEventReceiptIndex": {}, + "bridgeReplayIndex": {}, "challenges": { "challenge:demo:001": { "challengeId": "challenge:demo:001", @@ -98,7 +110,7 @@ "finalizedBy": "operator:local-demo", "receiptId": "receipt:demo:001", "rootfieldId": "rootfield:demo:alpha", - "stateRoot": "0x4e7ee5e7a8cab9b4ddda183842b9e9c1e1e000afea820b577ecc90fa4d9517e2" + "stateRoot": "0xf47b94f6d4be36bc5ade63085c9c3c6174d5b25eb5bfaae9a57e3946f5637352" } }, "genesisConfig": { @@ -134,11 +146,17 @@ "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", "balanceTransferRoot": "0x9b6e249f769a93bc9f34a90156e028d1a830badcd8ccdc5b1487d512cdbf0a6d", - "baseAnchorRoot": "0xa10b087464d8e6098696295a2a4b26a4396974c9ed10dd0bba429f22284cd573", + "baseAnchorRoot": "0x8b7c3423523f44b9a470a1281b088c72537a42e26f88fc8cd4c3e8360324c7ca", + "bridgeAccountMappingRoot": "0x46285ec5782d3e6cc1e557fd5dd779e979d679e5cc723832cb189d27a5c552c8", + "bridgeAssetMappingRoot": "0xbd283f6190cba76038f7364e207557dd20f7883d78e5f89a96579af5455a89aa", + "bridgeCreditReceiptRoot": "0x0a86ce98161cb40ca4c7719468c0ed8f122677d12641c13305277094b8f3a509", + "bridgeCreditRoot": "0x613ab06f110cc7a5eb06a0f44b7a0f04b0b3cfb0556d30d42842f33563596248", + "bridgeEventReceiptIndexRoot": "0xcaad305c45f57c516d0fdfdef15c5d88b399b976df173f005e9d7dc30fbdbdb2", + "bridgeReplayIndexRoot": "0x9344f8651651d2d5042d2853205154a5777cd57aa9e892e2ac41b9b9fe04c00f", "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", "dexPoolRoot": "0x0e5f034494a2deb6a4f20c04f01e678795c587df4869ad3f189f107fcc447dea", "faucetRecordRoot": "0x2277503a52fab3f9e49b40debfb7d641abee75cf268aa56da403fdcf4fad6cee", - "finalityReceiptRoot": "0xdf352e20fd1ddfd2855202335e03cfec21d87e99bf8717d161fe8648998e16cf", + "finalityReceiptRoot": "0x1c8536e507c5ba2242a5875c9537043645828825286ca256cc1171f1eb960c3f", "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", "liquidityReceiptRoot": "0x98bf15cc6859994038744612e125236b1f777895a051c41702c2004134327738", @@ -210,7 +228,7 @@ } }, "schema": "flowmemory.dashboard_state.local_devnet.v0", - "stateRoot": "0x3074ef2e5311d94e8f9a2660a6cc016c7b7f9a08c56ee07f9e841c1489726e68", + "stateRoot": "0xb3c4344483c7be8c0660d49d465d033a88ae26d6a7c1cbe096a2447d32c5648c", "swapReceipts": {}, "tokenBalances": {}, "tokenDefinitions": {}, diff --git a/apps/dashboard/public/data/flowchain-local-devnet-state.json b/apps/dashboard/public/data/flowchain-local-devnet-state.json index 080bdd3f..d8fc300e 100644 --- a/apps/dashboard/public/data/flowchain-local-devnet-state.json +++ b/apps/dashboard/public/data/flowchain-local-devnet-state.json @@ -19,7 +19,7 @@ "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", "nextBlockNumber": 3, "logicalTime": 1778688002, - "parentHash": "0x7ddb184c69f798f25f27a254f1f530c6cdc31c9656ac19d1b8c114f7a3a650c6", + "parentHash": "0x64e3d605fb1bba40b7de6a6a9813f77619282c07b9a33c7b41df1f6e3d71958b", "operatorKeyReferences": { "operator-key:local-devnet:alpha": { "schema": "flowmemory.local_devnet.operator_key_reference.v0", @@ -89,6 +89,12 @@ "lpPositions": {}, "liquidityReceipts": {}, "swapReceipts": {}, + "bridgeAssetMappings": {}, + "bridgeAccountMappings": {}, + "bridgeCredits": {}, + "bridgeCreditReceipts": {}, + "bridgeReplayIndex": {}, + "bridgeEventReceiptIndex": {}, "modelPassports": { "model:demo:local-alpha": { "modelPassportId": "model:demo:local-alpha", @@ -135,7 +141,7 @@ "finalityStatus": "finalized", "challengeCount": 1, "finalizedAtBlock": 1, - "stateRoot": "0x4e7ee5e7a8cab9b4ddda183842b9e9c1e1e000afea820b577ecc90fa4d9517e2" + "stateRoot": "0xf47b94f6d4be36bc5ade63085c9c3c6174d5b25eb5bfaae9a57e3946f5637352" } }, "artifactCommitments": { @@ -193,12 +199,12 @@ "importedObservations": {}, "importedVerifierReports": {}, "baseAnchors": { - "0xf57ab7d2c1459c03cf01bfddd56b046be685d8eaa4597e6bb54b5015aeaf003f": { - "anchorId": "0xf57ab7d2c1459c03cf01bfddd56b046be685d8eaa4597e6bb54b5015aeaf003f", + "0x6908eac7dbf828ab8d295d2ae86f0b0db9295a27b1fe4999c26a3ed97244ef8d": { + "anchorId": "0x6908eac7dbf828ab8d295d2ae86f0b0db9295a27b1fe4999c26a3ed97244ef8d", "appchainChainId": "flowmemory-local-devnet-v0", "blockRangeStart": 1, "blockRangeEnd": 1, - "stateRoot": "0x8c7c1e7a078b60a809d17a51c44e275059afb8d7535769430c3fc9e9320c7e23", + "stateRoot": "0x7e50021bc0dd2537f9ecfd7973f9799b6ffdffcfd427e3bb059f4dfd1253b947", "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023", "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", @@ -215,10 +221,16 @@ "lpPositionRoot": "0xe67dd98259afb06ca93620b4a8742b924ec2e8f3e6e72934eef5b8e60829d46f", "liquidityReceiptRoot": "0x98bf15cc6859994038744612e125236b1f777895a051c41702c2004134327738", "swapReceiptRoot": "0xc9f3ee93962f36ed10421ec9ad736079a3b13ef6504336495af243b718cefee1", + "bridgeAssetMappingRoot": "0xbd283f6190cba76038f7364e207557dd20f7883d78e5f89a96579af5455a89aa", + "bridgeAccountMappingRoot": "0x46285ec5782d3e6cc1e557fd5dd779e979d679e5cc723832cb189d27a5c552c8", + "bridgeCreditRoot": "0x613ab06f110cc7a5eb06a0f44b7a0f04b0b3cfb0556d30d42842f33563596248", + "bridgeCreditReceiptRoot": "0x0a86ce98161cb40ca4c7719468c0ed8f122677d12641c13305277094b8f3a509", + "bridgeReplayIndexRoot": "0x9344f8651651d2d5042d2853205154a5777cd57aa9e892e2ac41b9b9fe04c00f", + "bridgeEventReceiptIndexRoot": "0xcaad305c45f57c516d0fdfdef15c5d88b399b976df173f005e9d7dc30fbdbdb2", "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", - "finalityReceiptRoot": "0xdf352e20fd1ddfd2855202335e03cfec21d87e99bf8717d161fe8648998e16cf", + "finalityReceiptRoot": "0x1c8536e507c5ba2242a5875c9537043645828825286ca256cc1171f1eb960c3f", "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -325,13 +337,13 @@ "error": null } ], - "stateRoot": "0x8c7c1e7a078b60a809d17a51c44e275059afb8d7535769430c3fc9e9320c7e23", - "blockHash": "0x61e9f90b982f13988e85a382fc39da82c9114ecceea9001ab454c744e0801a9b" + "stateRoot": "0x7e50021bc0dd2537f9ecfd7973f9799b6ffdffcfd427e3bb059f4dfd1253b947", + "blockHash": "0xd890758bdeaa4ab7a658d7813b2b9bdd0850824aaef830b9f2f4ff455aaf3811" }, { "schema": "flowmemory.local_devnet.block.v0", "blockNumber": 2, - "parentHash": "0x61e9f90b982f13988e85a382fc39da82c9114ecceea9001ab454c744e0801a9b", + "parentHash": "0xd890758bdeaa4ab7a658d7813b2b9bdd0850824aaef830b9f2f4ff455aaf3811", "logicalTime": 1778688001, "txIds": [ "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" @@ -343,8 +355,8 @@ "error": null } ], - "stateRoot": "0x3074ef2e5311d94e8f9a2660a6cc016c7b7f9a08c56ee07f9e841c1489726e68", - "blockHash": "0x7ddb184c69f798f25f27a254f1f530c6cdc31c9656ac19d1b8c114f7a3a650c6" + "stateRoot": "0xb3c4344483c7be8c0660d49d465d033a88ae26d6a7c1cbe096a2447d32c5648c", + "blockHash": "0x64e3d605fb1bba40b7de6a6a9813f77619282c07b9a33c7b41df1f6e3d71958b" } ], "pendingTxs": [] diff --git a/apps/dashboard/public/data/flowmemory-dashboard-v0.json b/apps/dashboard/public/data/flowmemory-dashboard-v0.json index b226d982..01343d34 100644 --- a/apps/dashboard/public/data/flowmemory-dashboard-v0.json +++ b/apps/dashboard/public/data/flowmemory-dashboard-v0.json @@ -1993,11 +1993,11 @@ ], "devnetBlocks": [ { - "id": "0x61e9f90b982f13988e85a382fc39da82c9114ecceea9001ab454c744e0801a9b", + "id": "0xd890758bdeaa4ab7a658d7813b2b9bdd0850824aaef830b9f2f4ff455aaf3811", "blockNumber": 1, - "blockHash": "0x61e9f90b982f13988e85a382fc39da82c9114ecceea9001ab454c744e0801a9b", + "blockHash": "0xd890758bdeaa4ab7a658d7813b2b9bdd0850824aaef830b9f2f4ff455aaf3811", "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", - "stateRoot": "0x8c7c1e7a078b60a809d17a51c44e275059afb8d7535769430c3fc9e9320c7e23", + "stateRoot": "0x7e50021bc0dd2537f9ecfd7973f9799b6ffdffcfd427e3bb059f4dfd1253b947", "receiptsRoot": "0x2f98caf4b28b2209cdf1f9beb1c23f8732c538657cc7a1d8855878b5400efabd", "timestamp": "2026-05-13T16:00:00.000Z", "observationCount": 8, @@ -2015,11 +2015,11 @@ } }, { - "id": "0x7ddb184c69f798f25f27a254f1f530c6cdc31c9656ac19d1b8c114f7a3a650c6", + "id": "0x64e3d605fb1bba40b7de6a6a9813f77619282c07b9a33c7b41df1f6e3d71958b", "blockNumber": 2, - "blockHash": "0x7ddb184c69f798f25f27a254f1f530c6cdc31c9656ac19d1b8c114f7a3a650c6", - "parentHash": "0x61e9f90b982f13988e85a382fc39da82c9114ecceea9001ab454c744e0801a9b", - "stateRoot": "0x3074ef2e5311d94e8f9a2660a6cc016c7b7f9a08c56ee07f9e841c1489726e68", + "blockHash": "0x64e3d605fb1bba40b7de6a6a9813f77619282c07b9a33c7b41df1f6e3d71958b", + "parentHash": "0xd890758bdeaa4ab7a658d7813b2b9bdd0850824aaef830b9f2f4ff455aaf3811", + "stateRoot": "0xb3c4344483c7be8c0660d49d465d033a88ae26d6a7c1cbe096a2447d32c5648c", "receiptsRoot": "0xa0407b9a8a55106d549e0f19b92fceaa7f7a25697e94ebf8a1fa74af7b9168f4", "timestamp": "2026-05-13T16:00:01.000Z", "observationCount": 8, diff --git a/apps/dashboard/scripts/sync-fixtures.mjs b/apps/dashboard/scripts/sync-fixtures.mjs index ccf709c3..c3f47b94 100644 --- a/apps/dashboard/scripts/sync-fixtures.mjs +++ b/apps/dashboard/scripts/sync-fixtures.mjs @@ -31,6 +31,11 @@ const fixtureCopies = [ source: resolve(repoRoot, "fixtures/bridge/base-sepolia-mock-deposit.json"), destination: resolve(destinationDir, "flowchain-bridge-test-deposit.json"), }, + { + label: "FlowChain L1 explorer fallback", + source: resolve(repoRoot, "fixtures/dashboard/flowchain-l1-explorer-fallback.json"), + destination: resolve(destinationDir, "flowchain-l1-explorer-fallback.json"), + }, ]; mkdirSync(destinationDir, { recursive: true }); diff --git a/apps/dashboard/src/data/workbench.ts b/apps/dashboard/src/data/workbench.ts index c33e48d8..d4bb4688 100644 --- a/apps/dashboard/src/data/workbench.ts +++ b/apps/dashboard/src/data/workbench.ts @@ -4,10 +4,45 @@ export const DEFAULT_CONTROL_PLANE_URL = "http://127.0.0.1:8787"; export const WORKBENCH_DEVNET_STATE_PATH = "/data/flowchain-local-devnet-state.json"; export const WORKBENCH_DEVNET_DASHBOARD_STATE_PATH = "/data/flowchain-local-devnet-dashboard-state.json"; export const WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH = "/data/flowchain-bridge-test-deposit.json"; +export const WORKBENCH_EXPLORER_FALLBACK_PATH = "/data/flowchain-l1-explorer-fallback.json"; const FIXTURE_CHAIN_CONTEXT = "flowchain-private-local-testnet"; const CONTROL_PLANE_TIMEOUT_MS = 900; +const CONTROL_PLANE_RPC_REQUESTS = [ + { id: "chain_status", method: "chain_status" }, + { id: "node_status", method: "node_status" }, + { id: "peer_list", method: "peer_list", params: { limit: 50 } }, + { id: "block_list", method: "block_list", params: { limit: 50, includeTransactions: true } }, + { id: "transaction_list", method: "transaction_list", params: { limit: 50 } }, + { id: "mempool_list", method: "mempool_list", params: { limit: 50 } }, + { id: "account_list", method: "account_list", params: { limit: 50 } }, + { id: "wallet_metadata_list", method: "wallet_metadata_list", params: { limit: 50 } }, + { id: "token_list", method: "token_list", params: { limit: 50 } }, + { id: "token_balance_list", method: "token_balance_list", params: { limit: 50 } }, + { id: "token_transfer_list", method: "token_transfer_list", params: { limit: 50 } }, + { id: "pool_list", method: "pool_list", params: { limit: 50 } }, + { id: "lp_position_list", method: "lp_position_list", params: { limit: 50 } }, + { id: "swap_list", method: "swap_list", params: { limit: 50 } }, + { id: "receipt_list", method: "receipt_list", params: { limit: 50 } }, + { id: "work_receipt_list", method: "work_receipt_list", params: { limit: 50 } }, + { id: "finality_list", method: "finality_list", params: { limit: 50 } }, + { id: "bridge_observation_list", method: "bridge_observation_list", params: { limit: 50 } }, + { id: "bridge_deposit_list", method: "bridge_deposit_list", params: { limit: 50 } }, + { id: "bridge_credit_list", method: "bridge_credit_list", params: { limit: 50 } }, + { id: "withdrawal_list", method: "withdrawal_list", params: { limit: 50 } }, + { id: "pilot_deposit_observation_list", method: "pilot_deposit_observation_list", params: { limit: 50 } }, + { id: "pilot_credit_list", method: "pilot_credit_list", params: { limit: 50 } }, + { id: "pilot_withdrawal_intent_list", method: "pilot_withdrawal_intent_list", params: { limit: 50 } }, + { id: "pilot_release_evidence_list", method: "pilot_release_evidence_list", params: { limit: 50 } }, + { id: "pilot_cap_status", method: "pilot_cap_status" }, + { id: "pilot_pause_status", method: "pilot_pause_status" }, + { id: "pilot_retry_status", method: "pilot_retry_status" }, + { id: "pilot_emergency_status", method: "pilot_emergency_status" }, + { id: "product_flow_status", method: "product_flow_status" }, + { id: "raw_json_explorer_fallback", method: "raw_json_get", params: { source: "explorerFallback" } }, +] as const; + export type WorkbenchSource = "control-plane" | "fixture-fallback"; export type WorkbenchSectionKey = | "nodeStatus" @@ -21,9 +56,11 @@ export type WorkbenchSectionKey = | "walletMetadata" | "tokenLaunches" | "tokenBalances" + | "tokenTransfers" | "dexPools" | "liquidityPositions" | "swaps" + | "receiptEvents" | "explorerRecords" | "realValuePilot" | "rootfields" @@ -39,6 +76,8 @@ export type WorkbenchSectionKey = | "bridgeDeposits" | "bridgeCredits" | "bridgeWithdrawals" + | "bridgeReleases" + | "errorsRecovery" | "provenance" | "hardwareSignals" | "rawJson"; @@ -77,6 +116,7 @@ export interface ControlPlaneProbe { health?: unknown; state?: unknown; pilotStatus?: unknown; + rpc?: Record; } export interface WorkbenchNodeStatus { @@ -114,9 +154,11 @@ export interface WorkbenchSnapshot { devnetState: unknown | null; devnetDashboardState: unknown | null; bridgeTestDeposit: unknown | null; + explorerFallback: unknown | null; controlPlanePilotStatus: unknown | null; controlPlaneHealth: unknown | null; controlPlaneState: unknown | null; + controlPlaneRpc: Record | null; }; } @@ -211,6 +253,14 @@ export const WORKBENCH_SECTIONS: WorkbenchSectionDefinition[] = [ missingCommand: "npm run flowchain:product-e2e", missingService: "FlowChain token balance view /token-balances", }, + { + key: "tokenTransfers", + label: "Token Transfers", + detail: "Local/testnet token transfer records by account, token, and transaction id.", + expectedEndpoint: "POST /rpc token_transfer_list", + missingCommand: "npm run flowchain:product-e2e", + missingService: "FlowChain token transfer view token_transfer_list", + }, { key: "dexPools", label: "DEX Pools", @@ -283,6 +333,14 @@ export const WORKBENCH_SECTIONS: WorkbenchSectionDefinition[] = [ missingCommand: "npm run flowchain:smoke", missingService: "FlowChain receipt view /receipts", }, + { + key: "receiptEvents", + label: "Receipts / Events", + detail: "Transaction receipts, FlowPulse events, rejected logs, and failed transaction errors indexed by block and transaction.", + expectedEndpoint: "POST /rpc receipt_list + transaction_list", + missingCommand: "npm run control-plane:smoke", + missingService: "FlowChain receipt/event explorer methods", + }, { key: "memoryCells", label: "Memory Cells", @@ -355,6 +413,22 @@ export const WORKBENCH_SECTIONS: WorkbenchSectionDefinition[] = [ missingCommand: "npm run flowchain:smoke", missingService: "FlowChain bridge withdrawal view /bridge/withdrawals", }, + { + key: "bridgeReleases", + label: "Bridge Releases", + detail: "Release evidence for withdrawal intents, including pending or recorded operator evidence rows.", + expectedEndpoint: "POST /rpc pilot_release_evidence_list", + missingCommand: "npm run flowchain:real-value-pilot:e2e", + missingService: "FlowChain pilot release evidence list", + }, + { + key: "errorsRecovery", + label: "Errors / Recovery", + detail: "Runtime, API, storage, bridge, chain-id, lockbox, broad-scan, duplicate-event, and build recovery references.", + expectedEndpoint: "GET /health + fallback errors", + missingCommand: "npm run control-plane:smoke", + missingService: "FlowChain error and recovery state", + }, { key: "provenance", label: "Provenance / Source", @@ -494,16 +568,16 @@ function stringArray(value: unknown): string[] { function statusFrom(value: unknown, fallback: DashboardStatus = "observed"): DashboardStatus { const normalized = text(value, fallback).toLowerCase(); - if (normalized === "applied" || normalized === "success" || normalized === "active" || normalized === "live") { + if (normalized === "applied" || normalized === "success" || normalized === "active" || normalized === "live" || normalized === "recorded") { return "verified"; } if (normalized === "finalized") { return "finalized"; } - if (normalized === "failed" || normalized === "invalid" || normalized === "reverted") { + if (normalized === "failed" || normalized === "invalid" || normalized === "reverted" || normalized === "rejected" || normalized.includes("rejected")) { return "failed"; } - if (normalized === "pending" || normalized === "local-placeholder" || normalized === "degraded") { + if (normalized === "pending" || normalized === "requested" || normalized === "local-placeholder" || normalized === "degraded") { return "pending"; } if (normalized === "error") { @@ -518,7 +592,7 @@ function statusFrom(value: unknown, fallback: DashboardStatus = "observed"): Das if (normalized === "reorged") { return "reorged"; } - if (normalized === "unresolved") { + if (normalized === "unresolved" || normalized === "blocked") { return "unresolved"; } if (normalized === "offline") { @@ -669,12 +743,12 @@ function getControlPlaneUrl(): string { return configured && configured.length > 0 ? configured.replace(/\/+$/, "") : DEFAULT_CONTROL_PLANE_URL; } -async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { +async function fetchJsonWithTimeout(url: string, timeoutMs: number, init: RequestInit = {}): Promise { const controller = new AbortController(); const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs); try { - const response = await fetch(url, { cache: "no-store", signal: controller.signal }); + const response = await fetch(url, { ...init, cache: "no-store", signal: controller.signal }); if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`.trim()); } @@ -684,6 +758,31 @@ async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise> { + const payload = CONTROL_PLANE_RPC_REQUESTS.map((request) => ({ + jsonrpc: "2.0", + ...request, + })); + const response = await fetchJsonWithTimeout(`${url}/rpc`, CONTROL_PLANE_TIMEOUT_MS, { + method: "POST", + headers: { + "content-type": "application/json", + accept: "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!Array.isArray(response)) { + throw new Error("control-plane /rpc batch did not return an array"); + } + + return Object.fromEntries( + response + .filter(isRecord) + .map((entry) => [text(entry.id), isRecord(entry.error) ? { error: entry.error } : entry.result ?? null]), + ); +} + async function fetchOptionalJson(path: string): Promise<{ value: unknown | null; error?: string }> { try { return { value: await fetchJsonWithTimeout(path, CONTROL_PLANE_TIMEOUT_MS) }; @@ -704,6 +803,7 @@ async function probeControlPlane(): Promise { const health = await fetchJsonWithTimeout(`${url}/health`, CONTROL_PLANE_TIMEOUT_MS); let state: unknown | undefined; let pilotStatus: unknown | undefined; + let rpc: Record | undefined; try { pilotStatus = await fetchJsonWithTimeout(`${url}/pilot/status`, CONTROL_PLANE_TIMEOUT_MS); @@ -713,12 +813,17 @@ async function probeControlPlane(): Promise { try { state = await fetchJsonWithTimeout(`${url}/state`, CONTROL_PLANE_TIMEOUT_MS); + try { + rpc = await fetchControlPlaneRpc(url); + } catch { + rpc = undefined; + } } catch (error) { return { url, status: "available", checkedAt, - endpoints: uniqueEndpoints(defaultEndpoints, collectEndpointHints(health)), + endpoints: uniqueEndpoints(defaultEndpoints, ["POST /rpc"], collectEndpointHints(health)), health, pilotStatus, error: `Health endpoint responded, but state endpoint was not loaded: ${ @@ -731,10 +836,11 @@ async function probeControlPlane(): Promise { url, status: "available", checkedAt, - endpoints: uniqueEndpoints(defaultEndpoints, collectEndpointHints(health), collectEndpointHints(state)), + endpoints: uniqueEndpoints(defaultEndpoints, ["POST /rpc", "GET /explorer/search"], collectEndpointHints(health), collectEndpointHints(state)), health, state, pilotStatus, + rpc, }; } catch (error) { return { @@ -989,6 +1095,28 @@ function buildTokenBalanceRecords(devnetState: unknown): WorkbenchRecord[] { }); } +function buildTokenTransferRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["tokenTransfers", "tokenTransferEvents", "balanceTransfers", "transfers"]).map((transfer, index) => { + const id = text(transfer.transferId ?? transfer.tokenTransferId ?? transfer.txId ?? transfer.id, `token-transfer:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Token transfer", + title: id, + summary: text(transfer.summary, "Local/testnet token transfer exported by runtime, API, or deterministic fallback."), + status: statusFrom(transfer.status, "observed"), + facts: [ + { label: "tx", value: text(transfer.txId ?? transfer.transactionId) }, + { label: "token", value: text(transfer.tokenId ?? transfer.symbol) }, + { label: "from", value: text(transfer.fromAccount ?? transfer.from ?? transfer.sender) }, + { label: "to", value: text(transfer.toAccount ?? transfer.to ?? transfer.recipient) }, + { label: "amount", value: text(transfer.amount ?? transfer.units) }, + { label: "block", value: text(transfer.blockNumber ?? transfer.updatedAtBlock) }, + ], + raw: transfer, + }); + }); +} + function buildDexPoolRecords(devnetState: unknown): WorkbenchRecord[] { return collectionFrom(devnetState, ["dexPools", "pools", "ammPools", "liquidityPools"]).map((pool, index) => { const id = text(pool.poolId ?? pool.id ?? `${text(pool.baseToken ?? pool.tokenA)}:${text(pool.quoteToken ?? pool.tokenB)}`, `pool:${index + 1}`); @@ -1227,6 +1355,570 @@ function buildPilotRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { return records; } +function rpcPayload(controlPlane: ControlPlaneProbe, method: string): UnknownRecord | null { + const value = controlPlane.rpc?.[method]; + return isRecord(value) ? value : null; +} + +function rpcRows(controlPlane: ControlPlaneProbe, method: string, keys: string[]): UnknownRecord[] { + const payload = rpcPayload(controlPlane, method); + if (keys.length === 1 && keys[0] === "") { + return payload ? [payload] : []; + } + return collectionFrom(payload, keys); +} + +function makeRpcRecord( + controlPlane: ControlPlaneProbe, + method: string, + subsystem: SourceSubsystem, + record: Omit, +): WorkbenchRecord { + return makeLocalRecord(subsystem, `${controlPlane.url}/rpc:${method}`, record, controlPlane.checkedAt); +} + +function buildRpcBlockRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { + return rpcRows(controlPlane, "block_list", ["blocks"]).map((block, index) => + makeRpcRecord(controlPlane, "block_list", "devnet", { + id: text(block.blockHash ?? block.hash, `api-block:${index + 1}`), + kind: "Block", + title: `Block ${text(block.blockNumber ?? block.height)}`, + summary: `${text(block.txIds && Array.isArray(block.txIds) ? block.txIds.length : block.transactionCount, "0")} transactions, ${text(block.observationCount ?? block.eventCount ?? block.receiptCount, "0")} indexed events or receipts.`, + status: block.finalized === true ? "finalized" : statusFrom(block.status, "observed"), + facts: [ + { label: "height", value: text(block.blockNumber ?? block.height) }, + { label: "hash", value: text(block.blockHash ?? block.hash) }, + { label: "parent", value: text(block.parentHash) }, + { label: "state root", value: text(block.stateRoot) }, + { label: "source", value: text(block.source) }, + { label: "provenance", value: text(block.provenance ?? block.source) }, + ], + raw: block, + }), + ); +} + +function buildRpcTransactionRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { + return rpcRows(controlPlane, "transaction_list", ["transactions"]).map((tx, index) => + makeRpcRecord(controlPlane, "transaction_list", "indexer", { + id: text(tx.transactionId ?? tx.txHash ?? tx.txId, `api-tx:${index + 1}`), + kind: "Transaction", + title: text(tx.transactionId ?? tx.txHash ?? tx.txId, `api-tx:${index + 1}`), + summary: `${text(tx.type ?? tx.payloadType, "transaction")} is ${text(tx.status)} in block ${text(tx.blockNumber)}.`, + status: statusFrom(tx.status, "observed"), + facts: [ + { label: "block", value: text(tx.blockNumber) }, + { label: "signer", value: text(tx.signer ?? tx.accountId) }, + { label: "payload", value: text(tx.type ?? tx.payloadType) }, + { label: "receipt", value: text(tx.receiptId ?? tx.txHash) }, + { label: "error", value: text(tx.errorCode ?? tx.rejectedLogs, "none") }, + { label: "source", value: text(tx.source) }, + ], + raw: tx, + }), + ); +} + +function buildRpcTokenTransferRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { + return rpcRows(controlPlane, "token_transfer_list", ["transfers"]).map((transfer, index) => + makeRpcRecord(controlPlane, "token_transfer_list", "devnet", { + id: text(transfer.transferId ?? transfer.txId, `api-token-transfer:${index + 1}`), + kind: "Token transfer", + title: text(transfer.transferId ?? transfer.txId, `api-token-transfer:${index + 1}`), + summary: `${text(transfer.amount)} ${text(transfer.tokenId)} moved from ${text(transfer.fromAccount)} to ${text(transfer.toAccount)}.`, + status: statusFrom(transfer.status, "observed"), + facts: [ + { label: "tx", value: text(transfer.txId) }, + { label: "token", value: text(transfer.tokenId) }, + { label: "from", value: text(transfer.fromAccount) }, + { label: "to", value: text(transfer.toAccount) }, + { label: "amount", value: text(transfer.amount) }, + { label: "source", value: text(transfer.source) }, + ], + raw: transfer, + }), + ); +} + +function buildRpcRecords( + controlPlane: ControlPlaneProbe, + method: string, + keys: string[], + subsystem: SourceSubsystem, + kind: string, + idFields: string[], + factBuilder: (row: UnknownRecord) => WorkbenchFact[], +): WorkbenchRecord[] { + return rpcRows(controlPlane, method, keys).map((row, index) => { + const id = idFields.map((field) => text(row[field], "")).find((value) => value.length > 0 && value !== "not recorded") + ?? `${method}:${index + 1}`; + return makeRpcRecord(controlPlane, method, subsystem, { + id, + kind, + title: id, + summary: text(row.summary, `${kind} loaded from ${method}.`), + status: statusFrom(row.status ?? row.state, "observed"), + facts: factBuilder(row), + raw: row, + }); + }); +} + +function buildRpcReceiptEventRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { + const receipts = buildRpcRecords(controlPlane, "receipt_list", ["receipts"], "indexer", "Receipt", ["receiptId", "observationId"], (receipt) => [ + { label: "receipt", value: text(receipt.receiptId) }, + { label: "observation", value: text(receipt.observationId) }, + { label: "rootfield", value: text(receipt.rootfieldId) }, + { label: "verifier", value: text(receipt.verifierStatus) }, + { label: "flow memory", value: text(receipt.flowMemoryStatus) }, + { label: "report", value: text(receipt.reportId) }, + ]); + const txErrors = rpcRows(controlPlane, "transaction_list", ["transactions"]) + .filter((tx) => text(tx.status).toLowerCase().includes("reject") || text(tx.status).toLowerCase().includes("fail") || text(tx.errorCode, "").length > 0) + .map((tx, index) => + makeRpcRecord(controlPlane, "transaction_list", "indexer", { + id: text(tx.transactionId ?? tx.txHash, `api-tx-error:${index + 1}`), + kind: "Failed transaction", + title: text(tx.transactionId ?? tx.txHash, `api-tx-error:${index + 1}`), + summary: `Failure ${text(tx.errorCode ?? tx.status)} is visible from indexed transaction data.`, + status: "failed", + facts: [ + { label: "block", value: text(tx.blockNumber) }, + { label: "hash", value: text(tx.txHash ?? tx.transactionId) }, + { label: "error", value: text(tx.errorCode ?? tx.rejectedLogs) }, + { label: "source", value: text(tx.source) }, + ], + raw: tx, + }), + ); + return [...receipts, ...txErrors]; +} + +function buildRpcPilotListRecords( + controlPlane: ControlPlaneProbe, + method: string, + key: string, + kind: string, + idFields: string[], +): WorkbenchRecord[] { + return buildRpcRecords(controlPlane, method, [key], "devnet", kind, idFields, (row) => [ + { label: "chain", value: text(row.sourceChainId ?? row.destinationChainId, "8453") }, + { label: "tx", value: text(row.txHash ?? row.releaseTxHash) }, + { label: "account", value: text(row.accountId ?? row.flowchainRecipient ?? row.flowchainAccount) }, + { label: "amount", value: text(row.amount) }, + { label: "replay", value: text(row.replayStatus ?? row.rejectionReason, "accepted") }, + { label: "production ready", value: text(row.productionReady, "false") }, + ]); +} + +function buildRpcErrorRecoveryRecords(controlPlane: ControlPlaneProbe): WorkbenchRecord[] { + const fallbackRaw = rpcPayload(controlPlane, "raw_json_explorer_fallback"); + const fallbackData = isRecord(fallbackRaw?.raw) ? fallbackRaw.raw : null; + const errors = collectionFrom(fallbackData, ["errors"]).map((error) => + makeRpcRecord(controlPlane, "raw_json_get:explorerFallback", "alerts", { + id: text(error.errorId), + kind: "Recovery reference", + title: text(error.errorId), + summary: text(error.summary), + status: statusFrom(error.state, "pending"), + facts: [ + { label: "subsystem", value: text(error.subsystem) }, + { label: "state", value: text(error.state) }, + { label: "command", value: text(error.recoveryCommand) }, + ], + raw: error, + }), + ); + const health = rpcPayload(controlPlane, "health") ?? (isRecord(controlPlane.health) ? controlPlane.health : null); + if (health) { + errors.unshift( + makeRpcRecord(controlPlane, "health", "alerts", { + id: "control-plane-health", + kind: "Health", + title: text(health.status, "health"), + summary: controlPlane.error ? `Health loaded with issue: ${controlPlane.error}` : "Control-plane health response is visible.", + status: statusFrom(health.status, controlPlane.status === "available" ? "verified" : "offline"), + facts: [ + { label: "status", value: text(health.status) }, + { label: "missing", value: text(health.missingOptionalSources) }, + { label: "api", value: controlPlane.status }, + ], + raw: health, + }), + ); + } + if (errors.length === 0) { + errors.push( + makeLocalRecord("alerts", controlPlane.url, { + id: "api-offline", + kind: "Recovery reference", + title: "API offline", + summary: "Control-plane API is not detected; deterministic fallback remains visible.", + status: "offline", + facts: [ + { label: "command", value: "npm run control-plane:serve" }, + { label: "url", value: controlPlane.url }, + ], + raw: controlPlane, + }, controlPlane.checkedAt), + ); + } + return errors; +} + +function buildControlPlaneRpcSections( + controlPlane: ControlPlaneProbe, +): Partial> { + if (!controlPlane.rpc) { + return { + errorsRecovery: buildRpcErrorRecoveryRecords(controlPlane), + }; + } + + return { + nodeStatus: buildRpcRecords(controlPlane, "node_status", [""], "devnet", "Node status", ["nodeId"], (node) => [ + { label: "status", value: text(node.status) }, + { label: "latest height", value: text(node.latestBlockNumber) }, + { label: "latest hash", value: text(node.latestBlockHash) }, + { label: "peers", value: text(node.peerCount) }, + { label: "mempool", value: text(node.mempoolSize) }, + { label: "source", value: text(node.runtimeStateSource) }, + ]), + peers: buildRpcRecords(controlPlane, "peer_list", ["peers"], "devnet", "Peer", ["peerId", "address"], (peer) => [ + { label: "address", value: text(peer.address) }, + { label: "status", value: text(peer.status) }, + { label: "height", value: text(peer.height ?? peer.blockHeight) }, + { label: "role", value: text(peer.role) }, + ]), + blocks: buildRpcBlockRecords(controlPlane), + transactions: buildRpcTransactionRecords(controlPlane), + mempool: buildRpcRecords(controlPlane, "mempool_list", ["transactions"], "devnet", "Pending transaction", ["transactionId", "txId"], (tx) => [ + { label: "status", value: text(tx.status) }, + { label: "source", value: text(tx.source) }, + { label: "mode", value: text(tx.intakeMode) }, + ]), + accounts: buildRpcRecords(controlPlane, "account_list", ["accounts"], "devnet", "Account", ["accountId"], (account) => [ + { label: "type", value: text(account.accountType) }, + { label: "controller", value: text(account.controller) }, + { label: "balance", value: text(account.balance) }, + { label: "source", value: text(account.source) }, + ]), + walletMetadata: buildRpcRecords(controlPlane, "wallet_metadata_list", ["wallets"], "devnet", "Wallet public metadata", ["walletId", "accountId"], (wallet) => [ + { label: "account", value: text(wallet.accountId) }, + { label: "type", value: text(wallet.accountType) }, + { label: "public only", value: text(wallet.publicOnly, "true") }, + ]), + tokenLaunches: buildRpcRecords(controlPlane, "token_list", ["tokens"], "devnet", "Token", ["tokenId", "symbol"], (token) => [ + { label: "symbol", value: text(token.symbol) }, + { label: "name", value: text(token.name) }, + { label: "supply", value: text(token.totalSupply) }, + { label: "owner", value: text(token.owner) }, + { label: "source", value: text(token.source) }, + ]), + tokenBalances: buildRpcRecords(controlPlane, "token_balance_list", ["balances"], "devnet", "Token balance", ["balanceId", "accountId"], (balance) => [ + { label: "account", value: text(balance.accountId) }, + { label: "token", value: text(balance.tokenId) }, + { label: "amount", value: text(balance.amount) }, + { label: "source", value: text(balance.source) }, + ]), + tokenTransfers: buildRpcTokenTransferRecords(controlPlane), + dexPools: buildRpcRecords(controlPlane, "pool_list", ["pools"], "devnet", "DEX pool", ["poolId"], (pool) => [ + { label: "token 0", value: text(pool.token0) }, + { label: "token 1", value: text(pool.token1) }, + { label: "reserve 0", value: text(pool.reserve0) }, + { label: "reserve 1", value: text(pool.reserve1) }, + { label: "lp supply", value: text(pool.lpSupply) }, + { label: "source", value: text(pool.source) }, + ]), + liquidityPositions: buildRpcRecords(controlPlane, "lp_position_list", ["positions"], "devnet", "LP position", ["positionId", "accountId"], (position) => [ + { label: "account", value: text(position.accountId) }, + { label: "pool", value: text(position.poolId) }, + { label: "liquidity", value: text(position.liquidity) }, + { label: "source", value: text(position.source) }, + ]), + swaps: buildRpcRecords(controlPlane, "swap_list", ["swaps"], "devnet", "Swap", ["swapId", "txId"], (swap) => [ + { label: "tx", value: text(swap.txId) }, + { label: "pool", value: text(swap.poolId) }, + { label: "token in", value: text(swap.tokenIn) }, + { label: "amount in", value: text(swap.amountIn) }, + { label: "token out", value: text(swap.tokenOut) }, + { label: "amount out", value: text(swap.amountOut) }, + ]), + receipts: [ + ...buildRpcRecords(controlPlane, "work_receipt_list", ["workReceipts"], "worker", "WorkReceipt", ["workReceiptId", "receiptId"], (receipt) => [ + { label: "receipt", value: text(receipt.receiptId) }, + { label: "rootfield", value: text(receipt.rootfieldId) }, + { label: "status", value: text(receipt.status) }, + { label: "source", value: text(receipt.source) }, + ]), + ...buildRpcRecords(controlPlane, "receipt_list", ["receipts"], "worker", "MemoryReceipt", ["receiptId"], (receipt) => [ + { label: "observation", value: text(receipt.observationId) }, + { label: "rootfield", value: text(receipt.rootfieldId) }, + { label: "status", value: text(receipt.flowMemoryStatus) }, + { label: "report", value: text(receipt.reportId) }, + ]), + ], + receiptEvents: buildRpcReceiptEventRecords(controlPlane), + finality: buildRpcRecords(controlPlane, "finality_list", ["finality"], "devnet", "Finality", ["finalityId", "objectId", "receiptId"], (finality) => [ + { label: "object", value: text(finality.objectId) }, + { label: "rootfield", value: text(finality.rootfieldId) }, + { label: "status", value: text(finality.status) }, + { label: "source", value: text(finality.source) }, + ]), + bridgeDeposits: [ + ...buildRpcRecords(controlPlane, "bridge_observation_list", ["observations"], "devnet", "Bridge observation", ["observationId"], (row) => [ + { label: "deposit", value: text(row.depositId ?? (isRecord(row.deposit) ? row.deposit.depositId : undefined)) }, + { label: "tx", value: text(row.txHash ?? (isRecord(row.deposit) ? row.deposit.txHash : undefined)) }, + { label: "chain", value: text(row.sourceChainId ?? (isRecord(row.deposit) ? row.deposit.sourceChainId : undefined)) }, + { label: "replay", value: text(row.replayStatus, "accepted") }, + ]), + ...buildRpcPilotListRecords(controlPlane, "pilot_deposit_observation_list", "depositObservations", "Pilot deposit observation", ["observationId", "depositId"]), + ], + bridgeCredits: [ + ...buildRpcRecords(controlPlane, "bridge_credit_list", ["credits"], "devnet", "Bridge credit", ["creditId"], (credit) => [ + { label: "deposit", value: text(credit.depositId) }, + { label: "account", value: text(credit.accountId) }, + { label: "amount", value: text(credit.amount) }, + { label: "token", value: text(credit.token) }, + { label: "source", value: text(credit.source) }, + ]), + ...buildRpcPilotListRecords(controlPlane, "pilot_credit_list", "credits", "Pilot credit", ["creditId"]), + ], + bridgeWithdrawals: [ + ...buildRpcRecords(controlPlane, "withdrawal_list", ["withdrawals"], "devnet", "Withdrawal", ["withdrawalIntentId", "withdrawalId"], (withdrawal) => [ + { label: "credit", value: text(withdrawal.creditId) }, + { label: "deposit", value: text(withdrawal.depositId) }, + { label: "account", value: text(withdrawal.accountId) }, + { label: "amount", value: text(withdrawal.amount) }, + ]), + ...buildRpcPilotListRecords(controlPlane, "pilot_withdrawal_intent_list", "withdrawalIntents", "Pilot withdrawal intent", ["withdrawalIntentId"]), + ], + bridgeReleases: buildRpcPilotListRecords(controlPlane, "pilot_release_evidence_list", "releaseEvidence", "Release evidence", ["releaseEvidenceId", "withdrawalIntentId"]), + errorsRecovery: buildRpcErrorRecoveryRecords(controlPlane), + }; +} + +function explorerFallbackObjects(explorerFallback: unknown, key: string): UnknownRecord[] { + if (!isRecord(explorerFallback) || !isRecord(explorerFallback.objects)) { + return []; + } + + return recordValues(explorerFallback.objects[key]); +} + +function explorerFallbackBridgeRows(explorerFallback: unknown, key: string): UnknownRecord[] { + if (!isRecord(explorerFallback) || !isRecord(explorerFallback.bridge)) { + return []; + } + + return recordValues(explorerFallback.bridge[key]); +} + +function firstRecordText(record: UnknownRecord, keys: string[], fallback: string): string { + for (const key of keys) { + const value = text(record[key], ""); + if (value.length > 0) { + return value; + } + } + + return fallback; +} + +function makeExplorerFallbackRecord( + kind: string, + row: UnknownRecord, + index: number, + idKeys: string[], + facts: WorkbenchFact[], + summary: string, +): WorkbenchRecord { + const id = firstRecordText(row, idKeys, `${kind.toLowerCase().replace(/\s+/g, "-")}:${index + 1}`); + return makeRecord("devnet", WORKBENCH_EXPLORER_FALLBACK_PATH, { + id, + kind, + title: `${kind} ${id}`, + summary, + status: statusFrom(row.status ?? row.state), + facts: [ + ...facts, + { label: "provenance", value: "fixture-fallback" }, + ], + raw: row, + }); +} + +function buildExplorerFallbackSections(explorerFallback: unknown): Partial> { + if (!isRecord(explorerFallback)) { + return {}; + } + + const pilotReadiness = isRecord(explorerFallback.pilotReadiness) ? explorerFallback.pilotReadiness : null; + const replayProtection = isRecord(explorerFallback.bridge) && isRecord(explorerFallback.bridge.replayProtection) + ? explorerFallback.bridge.replayProtection + : null; + const observationRange = isRecord(pilotReadiness?.latestObservationBlockRange) + ? pilotReadiness.latestObservationBlockRange + : null; + const realValuePilot = pilotReadiness + ? [ + makeRecord("devnet", WORKBENCH_EXPLORER_FALLBACK_PATH, { + id: "explorer-fallback-base-pilot-readiness", + kind: "Base pilot readiness", + title: `Base pilot ${text(pilotReadiness.state, "degraded")}`, + summary: "Base 8453 pilot readiness, lockbox, cap, pause, emergency, observation range, and replay posture from deterministic fallback.", + status: statusFrom(pilotReadiness.state, "pending"), + facts: [ + { label: "source chain ID", value: text(pilotReadiness.baseChainId) }, + { label: "lockbox", value: text(pilotReadiness.lockboxAddress) }, + { label: "per deposit cap", value: text(pilotReadiness.perDepositCapUsd) }, + { label: "total cap", value: text(pilotReadiness.totalPilotCapUsd) }, + { label: "pause", value: text(pilotReadiness.pauseStatus) }, + { label: "emergency", value: text(pilotReadiness.emergencyStatus) }, + { label: "observation range", value: `${text(observationRange?.fromBlock)}-${text(observationRange?.toBlock)}` }, + { label: "confirmations", value: text(pilotReadiness.confirmationDepth) }, + { label: "duplicate replay keys", value: stringArray(replayProtection?.duplicateReplayKeys).length.toString() }, + ], + raw: { pilotReadiness, replayProtection }, + }), + ] + : []; + + const tokenLaunches = explorerFallbackObjects(explorerFallback, "tokens").map((token, index) => + makeExplorerFallbackRecord("Token", token, index, ["tokenId", "symbol"], [ + { label: "symbol", value: text(token.symbol) }, + { label: "name", value: text(token.name) }, + { label: "supply", value: text(token.totalSupply) }, + { label: "issuer", value: text(token.issuer ?? token.owner) }, + { label: "launch tx", value: text(token.launchTxId) }, + ], "Token launch record from deterministic explorer fallback."), + ); + const tokenBalances = explorerFallbackObjects(explorerFallback, "tokenBalances").map((balance, index) => + makeExplorerFallbackRecord("Token balance", balance, index, ["balanceId", "accountId"], [ + { label: "account", value: text(balance.accountId) }, + { label: "token", value: text(balance.tokenId) }, + { label: "amount", value: text(balance.amount) }, + { label: "updated block", value: text(balance.updatedAtBlock) }, + ], "Token balance record from deterministic explorer fallback."), + ); + const tokenTransfers = explorerFallbackObjects(explorerFallback, "tokenTransfers").map((transfer, index) => + makeExplorerFallbackRecord("Token transfer", transfer, index, ["transferId", "txId"], [ + { label: "tx", value: text(transfer.txId) }, + { label: "token", value: text(transfer.tokenId) }, + { label: "from", value: text(transfer.fromAccount) }, + { label: "to", value: text(transfer.toAccount) }, + { label: "amount", value: text(transfer.amount) }, + ], "Token transfer record from deterministic explorer fallback."), + ); + const dexPools = explorerFallbackObjects(explorerFallback, "pools").map((pool, index) => + makeExplorerFallbackRecord("DEX pool", pool, index, ["poolId"], [ + { label: "token 0", value: text(pool.token0) }, + { label: "token 1", value: text(pool.token1) }, + { label: "reserve 0", value: text(pool.reserve0) }, + { label: "reserve 1", value: text(pool.reserve1) }, + { label: "lp supply", value: text(pool.lpSupply) }, + ], "DEX pool record from deterministic explorer fallback."), + ); + const lpPositions = explorerFallbackObjects(explorerFallback, "lpPositions").map((position, index) => + makeExplorerFallbackRecord("LP position", position, index, ["positionId", "accountId"], [ + { label: "account", value: text(position.accountId) }, + { label: "pool", value: text(position.poolId) }, + { label: "liquidity", value: text(position.liquidity) }, + { label: "amount 0", value: text(position.amount0) }, + { label: "amount 1", value: text(position.amount1) }, + ], "LP position record from deterministic explorer fallback."), + ); + const liquidityEvents = explorerFallbackObjects(explorerFallback, "liquidityEvents").map((event, index) => + makeExplorerFallbackRecord("Liquidity event", event, index, ["liquidityEventId", "txId"], [ + { label: "tx", value: text(event.txId) }, + { label: "account", value: text(event.accountId) }, + { label: "pool", value: text(event.poolId) }, + { label: "action", value: text(event.action) }, + ], "Liquidity action record from deterministic explorer fallback."), + ); + const swaps = explorerFallbackObjects(explorerFallback, "swaps").map((swap, index) => + makeExplorerFallbackRecord("Swap", swap, index, ["swapId", "txId"], [ + { label: "tx", value: text(swap.txId) }, + { label: "account", value: text(swap.accountId) }, + { label: "pool", value: text(swap.poolId) }, + { label: "amount in", value: `${text(swap.amountIn)} ${text(swap.tokenIn)}` }, + { label: "amount out", value: `${text(swap.amountOut)} ${text(swap.tokenOut)}` }, + ], "Swap record from deterministic explorer fallback."), + ); + const bridgeDeposits = explorerFallbackBridgeRows(explorerFallback, "observations").map((observation, index) => { + const deposit: UnknownRecord = isRecord(observation.deposit) ? observation.deposit : {}; + return makeExplorerFallbackRecord("Bridge observation", observation, index, ["observationId"], [ + { label: "Base tx", value: text(deposit.txHash) }, + { label: "source chain", value: text(deposit.sourceChainId) }, + { label: "log index", value: text(deposit.logIndex) }, + { label: "lockbox", value: text(deposit.lockboxAddress ?? deposit.sourceContract) }, + { label: "recipient", value: text(deposit.flowchainRecipient) }, + { label: "replay status", value: text(deposit.status) }, + ], "Bridge deposit observation from deterministic explorer fallback."); + }); + const bridgeCredits = explorerFallbackBridgeRows(explorerFallback, "credits").map((credit, index) => + makeExplorerFallbackRecord("Bridge credit", credit, index, ["creditId"], [ + { label: "observation", value: text(credit.observationId) }, + { label: "deposit", value: text(credit.depositId) }, + { label: "recipient", value: text(credit.flowchainRecipient) }, + { label: "amount", value: text(credit.amount) }, + { label: "status", value: text(credit.status) }, + ], "Bridge credit record from deterministic explorer fallback."), + ); + const bridgeWithdrawals = [ + ...explorerFallbackBridgeRows(explorerFallback, "withdrawalIntents").map((withdrawal, index) => + makeExplorerFallbackRecord("Withdrawal intent", withdrawal, index, ["withdrawalIntentId"], [ + { label: "credit", value: text(withdrawal.creditId) }, + { label: "deposit", value: text(withdrawal.depositId) }, + { label: "account", value: text(withdrawal.flowchainAccount) }, + { label: "Base recipient", value: text(withdrawal.baseRecipient) }, + { label: "amount", value: text(withdrawal.amount) }, + ], "Bridge withdrawal intent from deterministic explorer fallback."), + ), + ...explorerFallbackObjects(explorerFallback, "withdrawals").map((withdrawal, index) => + makeExplorerFallbackRecord("Withdrawal", withdrawal, index, ["withdrawalIntentId", "withdrawalId"], [ + { label: "credit", value: text(withdrawal.creditId) }, + { label: "deposit", value: text(withdrawal.depositId) }, + { label: "account", value: text(withdrawal.accountId) }, + { label: "amount", value: text(withdrawal.amount) }, + ], "Local withdrawal record from deterministic explorer fallback."), + ), + ]; + const bridgeReleases = explorerFallbackBridgeRows(explorerFallback, "releaseEvidence").map((release, index) => + makeExplorerFallbackRecord("Release evidence", release, index, ["releaseEvidenceId"], [ + { label: "withdrawal", value: text(release.withdrawalIntentId) }, + { label: "credit", value: text(release.creditId) }, + { label: "deposit", value: text(release.depositId) }, + { label: "release tx", value: text(release.releaseTxHash) }, + { label: "status", value: text(release.status) }, + ], "Bridge release evidence from deterministic explorer fallback."), + ); + const errorsRecovery = isRecord(explorerFallback.errors) + ? recordValues(explorerFallback.errors).map((error, index) => + makeExplorerFallbackRecord("Recovery reference", error, index, ["errorId"], [ + { label: "subsystem", value: text(error.subsystem) }, + { label: "state", value: text(error.state) }, + { label: "command", value: text(error.recoveryCommand) }, + ], "Degraded-state recovery reference from deterministic explorer fallback."), + ) + : []; + + return { + tokenLaunches, + tokenBalances, + tokenTransfers, + dexPools, + liquidityPositions: [...lpPositions, ...liquidityEvents], + swaps, + bridgeDeposits, + bridgeCredits, + bridgeWithdrawals, + bridgeReleases, + realValuePilot, + errorsRecovery, + }; +} + function buildBlockRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { const blocks = collectionFrom(devnetState, ["blocks"]); @@ -1826,6 +2518,7 @@ function buildRawJsonRecords( devnetState: unknown | null, devnetDashboardState: unknown | null, bridgeTestDeposit: unknown | null, + explorerFallback: unknown | null, ): WorkbenchRecord[] { return [ makeRecord("indexer", data.metadata.fixturePath, { @@ -1881,6 +2574,20 @@ function buildRawJsonRecords( ], raw: bridgeTestDeposit, }), + makeRecord("devnet", WORKBENCH_EXPLORER_FALLBACK_PATH, { + id: "raw-explorer-fallback", + kind: "Raw JSON", + title: WORKBENCH_EXPLORER_FALLBACK_PATH, + summary: explorerFallback + ? "FlowChain L1 explorer fallback loaded for offline block, token, DEX, bridge, and recovery inspection." + : "FlowChain L1 explorer fallback was not loaded.", + status: explorerFallback ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(explorerFallback) ? text(explorerFallback.schema) : "missing" }, + { label: "keys", value: topLevelKeys(explorerFallback) }, + ], + raw: explorerFallback, + }), makeLocalRecord( "indexer", controlPlane.url, @@ -1897,11 +2604,13 @@ function buildRawJsonRecords( { label: "status", value: controlPlane.status }, { label: "health keys", value: topLevelKeys(controlPlane.health) }, { label: "state keys", value: topLevelKeys(controlPlane.state) }, + { label: "rpc keys", value: topLevelKeys(controlPlane.rpc) }, { label: "error", value: text(controlPlane.error, "none") }, ], raw: { health: controlPlane.health ?? null, state: controlPlane.state ?? null, + rpc: controlPlane.rpc ?? null, error: controlPlane.error ?? null, }, }, @@ -1916,6 +2625,7 @@ function buildProvenanceRecords( devnetState: unknown | null, devnetDashboardState: unknown | null, bridgeTestDeposit: unknown | null, + explorerFallback: unknown | null, ): WorkbenchRecord[] { return [ makeLocalRecord( @@ -1993,6 +2703,20 @@ function buildProvenanceRecords( ], raw: bridgeTestDeposit, }), + makeRecord("devnet", WORKBENCH_EXPLORER_FALLBACK_PATH, { + id: "flowchain-l1-explorer-fallback", + kind: "Explorer fixture", + title: WORKBENCH_EXPLORER_FALLBACK_PATH, + summary: explorerFallback + ? "FlowChain L1 explorer fallback is loaded with explicit fixture provenance." + : "FlowChain L1 explorer fallback was not loaded.", + status: explorerFallback ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(explorerFallback) ? text(explorerFallback.schema) : "missing" }, + { label: "source", value: "fixtures/dashboard/flowchain-l1-explorer-fallback.json" }, + ], + raw: explorerFallback, + }), ]; } @@ -2069,6 +2793,7 @@ export function buildWorkbenchSnapshot( devnetState?: unknown | null; devnetDashboardState?: unknown | null; bridgeTestDeposit?: unknown | null; + explorerFallback?: unknown | null; loadIssues?: string[]; } = {}, ): WorkbenchSnapshot { @@ -2084,6 +2809,8 @@ export function buildWorkbenchSnapshot( const controlPlaneState = extractControlPlaneState(controlPlane.state); const activeDevnetState = controlPlaneState ?? options.devnetState ?? null; const bridgeTestDeposit = options.bridgeTestDeposit ?? null; + const rawExplorerFallback = rpcPayload(controlPlane, "raw_json_explorer_fallback"); + const explorerFallback = options.explorerFallback ?? (isRecord(rawExplorerFallback?.raw) ? rawExplorerFallback.raw : null); const source: WorkbenchSource = controlPlane.status === "available" && controlPlaneState ? "control-plane" : "fixture-fallback"; const sections: Record = { @@ -2098,6 +2825,7 @@ export function buildWorkbenchSnapshot( walletMetadata: buildWalletMetadataRecords(activeDevnetState), tokenLaunches: buildTokenLaunchRecords(activeDevnetState), tokenBalances: buildTokenBalanceRecords(activeDevnetState), + tokenTransfers: buildTokenTransferRecords(activeDevnetState), dexPools: buildDexPoolRecords(activeDevnetState), liquidityPositions: buildLiquidityPositionRecords(activeDevnetState), swaps: buildSwapRecords(activeDevnetState), @@ -2106,6 +2834,7 @@ export function buildWorkbenchSnapshot( agents: buildAgentRecords(data, activeDevnetState), models: buildModelRecords(activeDevnetState), receipts: buildReceiptRecords(data, activeDevnetState), + receiptEvents: [], memoryCells: buildMemoryCellRecords(data, activeDevnetState), artifacts: buildArtifactRecords(data, activeDevnetState), verifierModules: buildVerifierModuleRecords(data, activeDevnetState), @@ -2115,18 +2844,76 @@ export function buildWorkbenchSnapshot( bridgeDeposits: buildBridgeRecords(activeDevnetState, "deposits", bridgeTestDeposit), bridgeCredits: buildBridgeRecords(activeDevnetState, "credits", bridgeTestDeposit), bridgeWithdrawals: buildBridgeRecords(activeDevnetState, "withdrawals", bridgeTestDeposit), + bridgeReleases: [], realValuePilot: buildPilotRecords(controlPlane), + errorsRecovery: [], provenance: [], hardwareSignals: buildHardwareSignalRecords(data, activeDevnetState), rawJson: [], }; + const rpcSections = buildControlPlaneRpcSections(controlPlane); + for (const [key, records] of Object.entries(rpcSections) as Array<[WorkbenchSectionKey, WorkbenchRecord[] | undefined]>) { + if (records !== undefined && records.length > 0) { + sections[key] = records; + } + } + const explorerFallbackSections = buildExplorerFallbackSections(explorerFallback); + const supplementExplorerFallback = new Set([ + "bridgeDeposits", + "bridgeCredits", + "bridgeWithdrawals", + "bridgeReleases", + "realValuePilot", + "errorsRecovery", + ]); + for (const [key, records] of Object.entries(explorerFallbackSections) as Array<[WorkbenchSectionKey, WorkbenchRecord[] | undefined]>) { + if (records === undefined || records.length === 0) { + continue; + } + + if (sections[key].length === 0) { + sections[key] = records; + } else if (source !== "control-plane" && supplementExplorerFallback.has(key)) { + sections[key] = [...records, ...sections[key]]; + } + } + sections.receiptEvents = sections.receiptEvents.length > 0 + ? sections.receiptEvents + : buildReceiptRecords(data, activeDevnetState).slice(0, 8); + sections.bridgeReleases = sections.bridgeReleases.length > 0 + ? sections.bridgeReleases + : buildPilotRecords(controlPlane).filter((record) => record.kind.toLowerCase().includes("release")); + sections.errorsRecovery = sections.errorsRecovery.length > 0 + ? sections.errorsRecovery + : buildRpcErrorRecoveryRecords(controlPlane); + sections.explorerRecords = [ + ...sections.blocks.slice(0, 4), + ...sections.transactions.slice(0, 8), + ...sections.receiptEvents.slice(0, 6), + ...sections.tokenLaunches.slice(0, 4), + ...sections.tokenTransfers.slice(0, 4), + ...sections.dexPools.slice(0, 4), + ...sections.liquidityPositions.slice(0, 4), + ...sections.swaps.slice(0, 4), + ...sections.bridgeDeposits.slice(0, 4), + ...sections.bridgeCredits.slice(0, 4), + ...sections.bridgeWithdrawals.slice(0, 4), + ...sections.bridgeReleases.slice(0, 4), + ].map((record) => ({ + ...record, + id: record.id.startsWith("explorer:") ? record.id : `explorer:${record.kind}:${record.id}`, + kind: record.kind.startsWith("Explorer ") ? record.kind : `Explorer ${record.kind}`, + summary: record.summary.startsWith("Explorer index projection:") ? record.summary : `Explorer index projection: ${record.summary}`, + })); + sections.provenance = buildProvenanceRecords( data, controlPlane, options.devnetState ?? null, options.devnetDashboardState ?? null, bridgeTestDeposit, + explorerFallback, ); sections.rawJson = buildRawJsonRecords( data, @@ -2134,6 +2921,7 @@ export function buildWorkbenchSnapshot( options.devnetState ?? null, options.devnetDashboardState ?? null, bridgeTestDeposit, + explorerFallback, ); const displayedSections = source === "control-plane" ? relabelDevnetRecordsAsControlPlane(sections, controlPlane) : sections; @@ -2151,29 +2939,36 @@ export function buildWorkbenchSnapshot( devnetState: options.devnetState ?? null, devnetDashboardState: options.devnetDashboardState ?? null, bridgeTestDeposit, + explorerFallback, controlPlanePilotStatus: controlPlane.pilotStatus ?? null, controlPlaneHealth: controlPlane.health ?? null, controlPlaneState: controlPlane.state ?? null, + controlPlaneRpc: controlPlane.rpc ?? null, }, }; } export async function fetchWorkbenchSnapshot(data: DashboardData): Promise { - const [controlPlane, devnetStateResult, devnetDashboardStateResult, bridgeTestDepositResult] = await Promise.all([ + const [controlPlane, devnetStateResult, devnetDashboardStateResult, bridgeTestDepositResult, explorerFallbackResult] = await Promise.all([ probeControlPlane(), fetchOptionalJson(WORKBENCH_DEVNET_STATE_PATH), fetchOptionalJson(WORKBENCH_DEVNET_DASHBOARD_STATE_PATH), fetchOptionalJson(WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH), + fetchOptionalJson(WORKBENCH_EXPLORER_FALLBACK_PATH), ]); - const loadIssues = [devnetStateResult.error, devnetDashboardStateResult.error, bridgeTestDepositResult.error].filter( - (issue): issue is string => typeof issue === "string" && issue.length > 0, - ); + const loadIssues = [ + devnetStateResult.error, + devnetDashboardStateResult.error, + bridgeTestDepositResult.error, + explorerFallbackResult.error, + ].filter((issue): issue is string => typeof issue === "string" && issue.length > 0); return buildWorkbenchSnapshot(data, { controlPlane, devnetState: devnetStateResult.value, devnetDashboardState: devnetDashboardStateResult.value, bridgeTestDeposit: bridgeTestDepositResult.value, + explorerFallback: explorerFallbackResult.value, loadIssues, }); } diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index a26271c5..10025494 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -1451,6 +1451,8 @@ code { .workbench-switcher { position: static; grid-template-columns: repeat(2, minmax(0, 1fr)); + max-height: none; + overflow: visible; } } diff --git a/apps/dashboard/src/test/dashboardData.test.ts b/apps/dashboard/src/test/dashboardData.test.ts index f4e16394..1be798f7 100644 --- a/apps/dashboard/src/test/dashboardData.test.ts +++ b/apps/dashboard/src/test/dashboardData.test.ts @@ -3,6 +3,7 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import canaryFixture from "../../../../fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json"; import fixture from "../../../../fixtures/dashboard/flowmemory-dashboard-v0.json"; +import explorerFallback from "../../../../fixtures/dashboard/flowchain-l1-explorer-fallback.json"; import devnetDashboardState from "../../../../fixtures/launch-core/generated/devnet/dashboard-state.json"; import devnetState from "../../../../fixtures/launch-core/generated/devnet/state.json"; import bridgeTestDeposit from "../../public/data/flowchain-bridge-test-deposit.json"; @@ -15,6 +16,7 @@ import { WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, WORKBENCH_DEVNET_STATE_PATH, + WORKBENCH_EXPLORER_FALLBACK_PATH, WORKBENCH_SECTIONS, buildWorkbenchSnapshot, fetchWorkbenchSnapshot, @@ -112,6 +114,7 @@ describe("dashboard fixture", () => { devnetState, devnetDashboardState, bridgeTestDeposit, + explorerFallback, }); expect(workbench.source).toBe("fixture-fallback"); @@ -136,16 +139,20 @@ describe("dashboard fixture", () => { expect(workbench.sections.models.length).toBeGreaterThan(0); expect(workbench.sections.challenges.length).toBeGreaterThan(0); expect(workbench.sections.balances.length).toBeGreaterThan(0); - expect(workbench.sections.tokenLaunches).toHaveLength(0); - expect(workbench.sections.tokenBalances).toHaveLength(0); - expect(workbench.sections.dexPools).toHaveLength(0); - expect(workbench.sections.liquidityPositions).toHaveLength(0); - expect(workbench.sections.swaps).toHaveLength(0); + expect(workbench.sections.tokenLaunches).toHaveLength(1); + expect(workbench.sections.tokenBalances).toHaveLength(1); + expect(workbench.sections.tokenTransfers).toHaveLength(1); + expect(workbench.sections.dexPools).toHaveLength(1); + expect(workbench.sections.liquidityPositions.length).toBeGreaterThanOrEqual(2); + expect(workbench.sections.swaps).toHaveLength(1); expect(workbench.sections.bridgeDeposits.length).toBeGreaterThan(0); - expect(workbench.sections.bridgeCredits).toHaveLength(0); - expect(workbench.sections.bridgeWithdrawals).toHaveLength(0); + expect(workbench.sections.bridgeCredits).toHaveLength(2); + expect(workbench.sections.bridgeWithdrawals.length).toBeGreaterThanOrEqual(1); + expect(workbench.sections.bridgeReleases).toHaveLength(1); + expect(workbench.sections.errorsRecovery.length).toBeGreaterThanOrEqual(6); expect(workbench.sections.realValuePilot.length).toBeGreaterThan(0); - expect(workbench.sections.realValuePilot[0].facts.find((fact) => fact.label === "scope")?.value).toBe("capped owner testing"); + expect(workbench.sections.realValuePilot.some((record) => record.facts.some((fact) => fact.label === "scope" && fact.value === "capped owner testing"))).toBe(true); + expect(workbench.sections.realValuePilot.some((record) => record.facts.some((fact) => fact.label === "source chain ID" && fact.value === "8453"))).toBe(true); expect(workbench.sections.explorerRecords.length).toBeGreaterThan(0); expect(workbench.node.status).toBe("offline"); expect(workbench.actions).toEqual([]); @@ -259,6 +266,9 @@ describe("dashboard fixture", () => { if (url === WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH) { return Response.json(bridgeTestDeposit); } + if (url === WORKBENCH_EXPLORER_FALLBACK_PATH) { + return Response.json(explorerFallback); + } return new Response("not found", { status: 404 }); }); @@ -272,11 +282,13 @@ describe("dashboard fixture", () => { expect(workbench.raw.controlPlanePilotStatus).toMatchObject({ state: "degraded" }); expect(workbench.raw.devnetState).toEqual(devnetState); expect(workbench.raw.bridgeTestDeposit).toEqual(bridgeTestDeposit); + expect(workbench.raw.explorerFallback).toEqual(explorerFallback); expect(workbench.loadIssues).toEqual([]); expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:8787/health", expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:8787/pilot/status", expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_DEVNET_STATE_PATH, expect.any(Object)); expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_BRIDGE_TEST_DEPOSIT_PATH, expect.any(Object)); + expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_EXPLORER_FALLBACK_PATH, expect.any(Object)); }); it("renders the critical workbench view labels from fixture fallback", () => { @@ -284,6 +296,7 @@ describe("dashboard fixture", () => { devnetState, devnetDashboardState, bridgeTestDeposit, + explorerFallback, }); const html = renderToStaticMarkup(createElement(WorkbenchView, { data, workbench })); @@ -296,11 +309,15 @@ describe("dashboard fixture", () => { expect(html).toContain("Wallet Metadata"); expect(html).toContain("Token Launch"); expect(html).toContain("Token Balances"); + expect(html).toContain("Token Transfers"); expect(html).toContain("DEX Pools"); expect(html).toContain("Liquidity"); expect(html).toContain("Swaps"); + expect(html).toContain("Receipts / Events"); expect(html).toContain("Explorer Records"); expect(html).toContain("Bridge Deposits"); + expect(html).toContain("Bridge Releases"); + expect(html).toContain("Errors / Recovery"); expect(html).toContain("private keys in browser localStorage"); expect(html).toContain("Rootfields"); expect(html).toContain("Verifier Modules"); diff --git a/apps/dashboard/src/views/WorkbenchView.tsx b/apps/dashboard/src/views/WorkbenchView.tsx index 4815701b..d8bb71f0 100644 --- a/apps/dashboard/src/views/WorkbenchView.tsx +++ b/apps/dashboard/src/views/WorkbenchView.tsx @@ -51,13 +51,37 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps const [actionResult, setActionResult] = useState(null); const activeDefinition = WORKBENCH_SECTIONS.find((section) => section.key === activeSection) ?? WORKBENCH_SECTIONS[0]; const activeRecords = workbench.sections[activeSection] ?? []; + const allSearchRecords = useMemo( + () => + WORKBENCH_SECTIONS.flatMap((section) => + (workbench.sections[section.key] ?? []).map((record) => ({ + ...record, + id: `${section.key}:${record.id}`, + kind: `${section.label} / ${record.kind}`, + })), + ), + [workbench.sections], + ); + const searchActive = query.trim().length > 0; const filteredRecords = useMemo( - () => activeRecords.filter((record) => recordMatches(record, query)), - [activeRecords, query], + () => (searchActive ? allSearchRecords : activeRecords).filter((record) => recordMatches(record, query)), + [activeRecords, allSearchRecords, query, searchActive], ); + const displayDefinition = searchActive + ? { + ...activeDefinition, + label: "Search Results", + detail: + "Global loaded explorer search across blocks, transactions, receipts, accounts, tokens, pools, bridge observations, credits, withdrawal intents, and release evidence.", + expectedEndpoint: "POST /rpc explorer_search + loaded workbench index", + } + : activeDefinition; const sourceStatus: DashboardStatus = workbench.source === "control-plane" ? "verified" : "stale"; const bridgeRecordCount = - workbench.sections.bridgeDeposits.length + workbench.sections.bridgeCredits.length + workbench.sections.bridgeWithdrawals.length; + workbench.sections.bridgeDeposits.length + + workbench.sections.bridgeCredits.length + + workbench.sections.bridgeWithdrawals.length + + workbench.sections.bridgeReleases.length; const pilotRecords = workbench.sections.realValuePilot; const pilotOverview = pilotRecords[0]; const pilotState = pilotOverview?.facts.find((fact) => fact.label === "state")?.value ?? "degraded"; @@ -91,7 +115,7 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps label: "Token launch", detail: "Local/testnet token definitions and launch receipts.", command: "npm run flowchain:product-e2e", - count: workbench.sections.tokenLaunches.length + workbench.sections.tokenBalances.length, + count: workbench.sections.tokenLaunches.length + workbench.sections.tokenBalances.length + workbench.sections.tokenTransfers.length, Icon: ListChecks, }, { @@ -126,6 +150,14 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps count: bridgeRecordCount, Icon: ShieldAlert, }, + { + key: "errorsRecovery", + label: "Errors and recovery", + detail: "Offline, degraded, chain, lockbox, replay, and build recovery references.", + command: "npm run control-plane:smoke", + count: workbench.sections.errorsRecovery.length, + Icon: Terminal, + }, ]; const runLocalAction = async (endpoint: string, label: string) => { @@ -153,7 +185,7 @@ export function WorkbenchView({ data, workbench, onRefresh }: WorkbenchViewProps
{onRefresh ? (