diff --git a/.env.example b/.env.example index a3f91469..50e85624 100644 --- a/.env.example +++ b/.env.example @@ -7,10 +7,19 @@ BASE_SEPOLIA_RPC_URL= # Hex deployer key for Foundry script runs. Keep the real value out of Git. BASE_SEPOLIA_DEPLOYER_KEY_HEX= +# Optional explorer key for Base Sepolia source verification. Keep real values +# in your shell or ignored .env file only. +BASE_SEPOLIA_BASESCAN_API_KEY= +BASESCAN_API_KEY= + # Comma-separated FlowPulse-capable contract addresses for testnet reads. BASE_SEPOLIA_FLOWPULSE_ADDRESSES= BASE_SEPOLIA_FROM_BLOCK= BASE_SEPOLIA_TO_BLOCK= BASE_SEPOLIA_FINALIZED_BLOCK= +# Optional local artifact paths for rehearsal outputs. +BASE_SEPOLIA_REHEARSAL_PLAN_OUT=fixtures/deployments/base-sepolia-rehearsal-plan.json +BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT=fixtures/deployments/base-sepolia-rehearsal.latest.json + FLOWMEMORY_DASHBOARD_DATA_PATH=apps/dashboard/public/data/flowmemory-dashboard-v0.json diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index e3b72ed2..ae8e56df 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -47,6 +47,70 @@ npm run dev If the API is not running, the workbench marks the control-plane as offline, shows stale fixture fallback where appropriate, and keeps rendering deterministic local data. This app is for private/local validation and canary review only; it does not initiate value-bearing wallet flows. +## Beginner Demo Path + +The full launch-demo script lives in: + +```text +docs/LAUNCH_DEMO_RUNBOOK.md +``` + +From the repo root, the normal browser-demo setup is: + +```powershell +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +npm run flowchain:export +``` + +Then start the local API and workbench in separate PowerShell windows: + +```powershell +npm run control-plane:serve +``` + +```powershell +npm run workbench:dev +``` + +Open the Vite URL, usually: + +```text +http://127.0.0.1:5173/ +``` + +Click path for a short demo: + +1. **Workbench** (`/`) for local API status, setup path, object switcher, provenance, and raw private/local object views. +2. **Overview** (`/overview`) for local fixture metrics, recent FlowPulse observations, verifier attention, hardware risk, and devnet block window. +3. **Flow Memory** (`/flowmemory`) for MemorySignal, MemoryReceipt, RootfieldBundle, AgentMemoryView, and RootflowTransition state. +4. **Base canary** (`/canary`) for the isolated guarded canary review. This is not a production-readiness claim. +5. **Raw JSON** (`/raw`) only when a reviewer asks for the payload behind the UI. + +Workbench panel meanings: + +| Panel | Meaning | +| --- | --- | +| Node and API status | Local chain/API health, block height, state root, pending transaction count, and API URL. | +| Local setup path | Beginner command sequence for installing, refreshing launch fixtures, starting, smoking, and opening the workbench. | +| Control-plane endpoints and local actions | Local API endpoints and optional browser-safe actions when the API advertises them. | +| Workbench coverage metrics | Counts for data source, node views, chain objects, smoke objects, and challenges. | +| Object switcher | Clickable local object views such as blocks, transactions, agents, models, receipts, reports, memory cells, challenges, finality, bridge-shaped local test objects, provenance, hardware signals, and raw JSON. | + +Canary panel meanings: + +| Panel | Meaning | +| --- | --- | +| Canary boundary hero | Reminder that the current Base canary is visible on Base but still gated for production. | +| Reader command / review fixture / hard boundary strip | The reader shape, runtime fixture path, and no broad scan/no production/no real-funds boundary. | +| Canary metrics | Counts from the committed canary fixture, including rejected logs and duplicates. | +| Canary FlowPulse stream | Observed canary logs with block, pulse type, transaction hash, and log index. | +| FlowPulse contracts | Canary contracts that emitted FlowPulse logs in the review fixture. | +| Launch gates | Remaining boundaries before any production claim. | +| Canary Rootflow state | Rootflow transitions reconstructed from the canary observations. | +| Canary JSON | Full loaded canary dashboard payload for reviewer inspection. | + ## Data Boundary The canonical dashboard fixture is generated by the launch-core command and lives at: diff --git a/apps/dashboard/public/data/flowmemory-dashboard-base-canary-v0.json b/apps/dashboard/public/data/flowmemory-dashboard-base-canary-v0.json index d6ac2de2..e4e7c87d 100644 --- a/apps/dashboard/public/data/flowmemory-dashboard-base-canary-v0.json +++ b/apps/dashboard/public/data/flowmemory-dashboard-base-canary-v0.json @@ -1,7 +1,7 @@ { "metadata": { "schema": "flowmemory.dashboard.fixture.v0", - "generatedAt": "2026-05-13T20:05:51.625Z", + "generatedAt": "2026-05-13T23:49:07.586Z", "mode": "canary", "description": "Generated Base mainnet canary dashboard data from the guarded FlowPulse reader. It is canary-only and not a production-readiness claim.", "fixturePath": "fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json", @@ -136,7 +136,7 @@ "currentBlock": 45955540, "finalizedBlock": 45955540, "source": "live", - "lastUpdated": "2026-05-13T20:05:51.625Z" + "lastUpdated": "2026-05-13T23:49:07.586Z" }, "flowPulseObservations": [ { @@ -161,13 +161,13 @@ "uri": "flowmemory://base-canary/rootfield", "summary": "rootfield registration from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -193,13 +193,13 @@ "uri": "flowmemory://uniswap-v4/after-swap", "summary": "swap memory signal from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -225,13 +225,13 @@ "uri": "flowmemory://base-canary/root", "summary": "root commitment from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -257,13 +257,13 @@ "uri": "flowmemory://uniswap-v4/after-swap", "summary": "swap memory signal from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -284,13 +284,13 @@ ], "evidenceUri": "docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -331,13 +331,13 @@ "logIndex": "229" }, "id": "0xef6e3d4a6375fdaf3573db4550b7a6c4ac535f6ddbf8a2759014c7e6ee5dc17d", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -373,13 +373,13 @@ "logIndex": "274" }, "id": "0x7da37f62f68f29fdd92dde90fdb5783bce555c868bfb0181187ae955eb2d8c95", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -415,13 +415,13 @@ "logIndex": "364" }, "id": "0x508409804fe0dae77e8ced57cb45968877fcbad6f525a99b4f911ce58759eab7", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -457,13 +457,13 @@ "logIndex": "1212" }, "id": "0x91d0da9bc41fad8cb3fa66b72ba804f6685d34f0c167bd3fbf1ac48f91402188", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -534,13 +534,13 @@ ] }, "id": "0x1d530aa927338513f49fbd12145d05b1428347b5cfa0fb241033a9d070123637", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -609,13 +609,13 @@ ] }, "id": "0x4dbf295239f26095033ddbc39dbb3d2780a554ca697d3ea89c588bda449939d1", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -684,13 +684,13 @@ ] }, "id": "0x79640fe5901417b9254f9de76f011cdd54ffe9587aaf564f241f4b0cc2ec82da", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -759,13 +759,13 @@ ] }, "id": "0x623c146bc8899c5edadaed215cc86a1292f1f5c97acb245e86c0462190078ca5", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -804,13 +804,13 @@ "unsupported": 0, "reorged": 0 }, - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -842,13 +842,13 @@ "Source verification and operator policy must be completed before any production claim." ], "localOnly": false, - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "worker", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -862,7 +862,7 @@ "severity": "info", "title": "Canary mode", "summary": "Base mainnet canary logs are visible, but verifier reports, source verification, multisig ownership, and production hook wiring are not complete.", - "openedAt": "2026-05-13T20:05:51.625Z", + "openedAt": "2026-05-13T23:49:07.586Z", "linkedObjectIds": [ "0x2a7ADd68a1d45C3251E2F92fFe4926124654a97C", "0x179Df6d52e9DeF5D02704583a2E4E5a9FF427245", @@ -877,13 +877,13 @@ ], "recommendedAction": "Use this view for launch demonstrations and operator review only.", "status": "unresolved", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "alerts", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-v0.json" } } diff --git a/contracts/DEPLOYMENT_BOUNDARY.md b/contracts/DEPLOYMENT_BOUNDARY.md index 9aa2fa69..1254b476 100644 --- a/contracts/DEPLOYMENT_BOUNDARY.md +++ b/contracts/DEPLOYMENT_BOUNDARY.md @@ -121,17 +121,25 @@ Private keys must not be committed to the repo, copied into docs, or stored in g ## Current Commands ```powershell +npm run deploy:base-sepolia:plan -- --json npm run deploy:base-sepolia npm run deploy:base-sepolia:broadcast npm run read:base-sepolia -- --rpc-url --address --from-block --to-block +npm run read:base-sepolia -- --rpc-url --address --resume-from-checkpoint --to-block npm run verify:base-canary:sources -- --json ``` +`deploy:base-sepolia:plan` requires no private key and writes a non-secret +rehearsal plan to `fixtures/deployments/base-sepolia-rehearsal-plan.json`. + `deploy:base-sepolia` requires `BASE_SEPOLIA_RPC_URL` and `BASE_SEPOLIA_DEPLOYER_KEY_HEX` from the local shell or an untracked `.env` loader. The example file is `.env.example`; real key material must stay outside Git. +The detailed public testnet rehearsal runbook is +`docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md`. + `verify:base-canary:sources` reads `fixtures/deployments/base-canary-v0.json` and prints a dry-run verification plan by default. It also writes the same non-secret plan to diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index e5b47388..c11020e9 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -66,7 +66,7 @@ Indexer/verifier local package: - The control-plane API prefers live local runtime state from `devnet/local/`, falls back to deterministic fixtures, and exposes a 49-method local smoke client for node status, blocks, transactions, accounts, balances, wallets, Rootfields, receipts, verifier reports, memory cells, challenges, finality, bridge observations, bridge deposits, bridge credits, withdrawals, provenance, and raw JSON. - Control-plane transaction and bridge-observation intake writes local ignored files under `devnet/local/intake/` and rejects private-key, mnemonic, seed phrase, RPC credential, API key, and webhook-shaped material. - `npm run index:base-sepolia -- --rpc-url --address --from-block --to-block ` provides a constrained Base Sepolia reader path. -- The Base Sepolia reader requires an explicit RPC URL, rejects non-Base-Sepolia chain ids, and persists both canonical state and a durable checkpoint without storing RPC URLs or keys. +- The Base Sepolia reader requires an explicit RPC URL, rejects non-Base-Sepolia chain ids, rejects broad scans by default, validates resolved HTTP(S) RPC URLs, persists canonical state and a durable checkpoint atomically, and records resume fields without storing RPC URLs or keys. - `npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url --address --from-block --to-block ` provides a guarded Base mainnet canary reader path for the documented V0 canary deployment only. - The Base canary reader requires explicit acknowledgement, RPC URL, addresses, and block range; rejects non-Base-mainnet chain ids; refuses scans wider than 5,000 blocks; persists canonical state plus a durable canary checkpoint; and marks the checkpoint as not production-ready. - A live canary read over blocks `45955500` to `45955540` observed 4 FlowPulse logs from the documented `RootfieldRegistry` and `FlowMemoryHookAdapter` canary addresses with 0 rejected logs and 0 duplicates. @@ -74,7 +74,8 @@ Indexer/verifier local package: - The dashboard has a separate Base canary mode at `/canary` that shows live-read canary FlowPulse observations, Rootflow transitions, canary boundaries, and raw canary JSON without replacing local fixture mode. - `npm run verify:base-canary:sources` produces a dry-run source verification plan for all canary contracts and writes `fixtures/deployments/base-canary-source-verification-plan.json`; `npm run verify:base-canary:sources:submit` submits after `BASESCAN_API_KEY` is configured. - All 10 deployed Base canary contracts are verified on BaseScan. `FlowMemoryHookAdapter` was verified against deployment-source commit `11d562c` because `main` now contains the newer v4-shaped callback path. -- `npm run deploy:base-sepolia` and `npm run deploy:base-sepolia:broadcast` provide Foundry deploy commands for the current V0 Base Sepolia testnet contract set. They require local env values and do not commit credentials. +- `npm run deploy:base-sepolia:plan` writes a non-secret Base Sepolia rehearsal plan artifact. `npm run deploy:base-sepolia` and `npm run deploy:base-sepolia:broadcast` provide Foundry dry-run/broadcast commands for the current V0 Base Sepolia testnet contract set. They require local env values and do not commit credentials. +- `docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md` defines the dry-run, optional broadcast, write smoke, readback, source-verification, and rollback rehearsal path. - A Base mainnet V0 canary deployment exists for testing only; deployed addresses and smoke transactions are recorded in `docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md`. Dashboard V0: @@ -122,8 +123,10 @@ Launch-core specifications: - `docs/FLOWCHAIN_TESTNET_ACCEPTANCE.md` marks private/local testnet features as implemented, in flight, missing, or later gated. - `docs/FLOWCHAIN_AGENT_INTEGRATION_MAP.md` maps the next-wave worktree ownership and cross-agent handoffs. - `docs/FLOWCHAIN_TROUBLESHOOTING.md` and `docs/FLOWCHAIN_OPERATOR_CHECKLIST.md` provide the Windows-first second-computer troubleshooting and operator checklist layer. +- `docs/LAUNCH_DEMO_RUNBOOK.md` provides the beginner-safe browser click script, operator talk track, recovery steps, and launch-day demo checklist. - `docs/DECISIONS/rootflow-v0.md` records the V0 decision and non-goal boundaries. - `docs/reviews/ROOTFLOW_FLOW_MEMORY_V0_ACCEPTANCE_AUDIT.md` tracks evidence and missing work for the active launch-core goal. +- `docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md` records the current security boundary review for local/test V0 demos and guarded canary review. - `docs/reviews/OPEN_PR_MERGE_READINESS.md` is now historical merge-readiness evidence for PRs that have merged. - `docs/LAUNCH_CORE_AGENT_GOALS.md` provides copy-ready goals for the contracts, crypto, indexer/verifier, dashboard, and review worktrees. @@ -137,9 +140,8 @@ FlowChain private/local testnet snapshot: simulator, Base Sepolia reader/deploy commands, guarded canary reader, and Windows-first root wrapper commands for prerequisite checks, init, bounded start/stop, demo, smoke, full smoke, export/import, and workbench dev mode. -- In flight: private-testnet object IDs and envelopes for newest local balance - and bridge-shaped surfaces, workbench polish, optional hardware operator - signal fixtures, Base Sepolia hook deployment runbook, and advanced L1 +- In flight: optional hardware operator signal fixtures, Base Sepolia live + rehearsal execution against a funded testnet deployer, and advanced L1 research gates. - Missing: long-running multi-process node behavior, LAN peer mode, encrypted local operator vault, and second-computer smoke evidence for the latest diff --git a/docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md b/docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md index 477363b1..e5031e9d 100644 --- a/docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md +++ b/docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md @@ -113,7 +113,9 @@ Observed canary smoke read for the range above: - Observation count: `4`. - Rejected log count: `0`. - Duplicate count: `0`. -- Last indexed block: `45955535`. +- Highest observed block: `45955535`. +- Last indexed block scanned: `45955540`. +- Next checkpoint resume block: `45955541`. - Output state: `services/indexer/out/base-canary-indexer-state.json` by default. - Output checkpoint: `services/indexer/out/base-canary-indexer-checkpoint.json` by default. diff --git a/docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md b/docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md new file mode 100644 index 00000000..1004bf0d --- /dev/null +++ b/docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md @@ -0,0 +1,190 @@ +# Base Sepolia Deployment Rehearsal + +Status: V0 public testnet rehearsal path. + +This is not a production deployment guide, production mainnet guide, token +launch guide, custody guide, bridge guide, verifier-network guide, or +production Uniswap v4 hook guide. + +## Goal + +Use Base Sepolia to rehearse the current V0 contract package end to end: + +1. generate a non-secret deployment plan; +2. run a Foundry dry run with explicit local env values; +3. optionally broadcast to Base Sepolia after operator approval; +4. emit one tiny Rootfield/FlowPulse write path; +5. read the resulting FlowPulse logs with the Base Sepolia reader; +6. persist indexer state and checkpoint output; +7. record verification, rollback, and boundary notes. + +## Environment + +Use an ignored `.env` file or shell variables only. Do not paste real values into +docs, GitHub issues, PR comments, screenshots, or committed artifacts. + +Required for dry-run or broadcast: + +```powershell +$env:BASE_SEPOLIA_RPC_URL="" +$env:BASE_SEPOLIA_DEPLOYER_KEY_HEX="<0x-prefixed-32-byte-testnet-key>" +``` + +Optional: + +```powershell +$env:BASE_SEPOLIA_BASESCAN_API_KEY="" +$env:BASE_SEPOLIA_FLOWPULSE_ADDRESSES="" +$env:BASE_SEPOLIA_FROM_BLOCK="" +$env:BASE_SEPOLIA_TO_BLOCK="" +$env:BASE_SEPOLIA_FINALIZED_BLOCK="" +``` + +The repo also accepts `BASESCAN_API_KEY` as a fallback explorer key name. The +scripts redact key presence and never write the key value. + +## Plan-Only Command + +This command requires no private key and writes the non-secret plan artifact: + +```powershell +npm run deploy:base-sepolia:plan -- --json +``` + +Default output: + +```text +fixtures/deployments/base-sepolia-rehearsal-plan.json +``` + +The plan records: + +- required env names; +- contract names deployed by `DeployLaunchCandidate`; +- the redacted Foundry command; +- write smoke command templates; +- readback command templates; +- explorer verification command template; +- rollback notes and blocked claims. + +## Dry Run + +Run this before any Base Sepolia broadcast: + +```powershell +npm run deploy:base-sepolia -- --json +``` + +Default output when the dry run succeeds: + +```text +fixtures/deployments/base-sepolia-rehearsal.latest.json +``` + +The Foundry script now rejects the wrong chain id and expects Base Sepolia +`84532`. + +## Optional Broadcast + +Broadcast only after the operator approves testnet gas spend and confirms the +deployer key is a testnet key: + +```powershell +npm run deploy:base-sepolia:broadcast -- --json +``` + +After broadcast, copy only non-secret facts into a dated deployment doc: + +- deployer address; +- contract names and addresses; +- deploy transaction hashes; +- deployment blocks; +- source verification status; +- smoke transaction hashes; +- reader checkpoint paths. + +## Write Smoke + +Use small deterministic testnet values. These commands are templates; replace +addresses and hashes from the deployment artifact or Foundry broadcast output. + +```powershell +cast send "registerRootfield(bytes32,bytes32,bytes32,string)" "ipfs://flowmemory-base-sepolia-rehearsal" --rpc-url $env:BASE_SEPOLIA_RPC_URL --private-key $env:BASE_SEPOLIA_DEPLOYER_KEY_HEX +``` + +```powershell +cast send "submitRoot(bytes32,bytes32,bytes32,bytes32,string)" "ipfs://flowmemory-base-sepolia-rehearsal-root" --rpc-url $env:BASE_SEPOLIA_RPC_URL --private-key $env:BASE_SEPOLIA_DEPLOYER_KEY_HEX +``` + +Optional hook-adapter smoke: + +```powershell +cast send "afterSwap(address,bytes32,bytes32,bytes32,bytes)" 0x1234 --rpc-url $env:BASE_SEPOLIA_RPC_URL --private-key $env:BASE_SEPOLIA_DEPLOYER_KEY_HEX +``` + +These writes are for public testnet rehearsal only. They do not prove +production hook readiness. + +## Readback + +Read contract state: + +```powershell +cast call "getRootfield(bytes32)" --rpc-url $env:BASE_SEPOLIA_RPC_URL +``` + +Read FlowPulse logs: + +```powershell +npm run index:base-sepolia -- --rpc-url $env:BASE_SEPOLIA_RPC_URL --address --address --from-block --to-block --finalized-block +``` + +Resume from the durable checkpoint: + +```powershell +npm run index:base-sepolia -- --rpc-url $env:BASE_SEPOLIA_RPC_URL --address --address --resume-from-checkpoint --to-block +``` + +The reader: + +- requires an explicit RPC URL; +- rejects non-Base-Sepolia chain ids; +- rejects broad scans above the configured span; +- stores state and checkpoint JSON atomically; +- records `lastIndexedBlock`, `highestObservedBlock`, `nextFromBlock`, and + `emptyRange`; +- does not write RPC URLs, private keys, or explorer API keys. + +## Source Verification + +Use the explorer key only from local env: + +```powershell +forge verify-contract --chain-id 84532
--etherscan-api-key $env:BASE_SEPOLIA_BASESCAN_API_KEY +``` + +Record verification result per contract in the dated deployment doc. A verified +Base Sepolia source is still a testnet fact, not production approval. + +## Rollback And Recovery + +- If the dry run fails, fix locally and rerun the plan plus dry run. +- If broadcast fails before all contracts deploy, discard the partial address + set unless a reviewer explicitly approves documenting it as failed evidence. +- If write smoke fails, record the transaction hash and revert reason, then use + readback commands to distinguish contract failure from RPC/nonce failure. +- V0 has no proxy rollback. Stop using an address set by marking it superseded + in `docs/DEPLOYMENTS/`. +- Never reuse a deployer key that was exposed on screen or copied into docs. + +## Acceptance Checklist + +- `npm run deploy:base-sepolia:plan -- --json` writes a non-secret plan. +- `npm run deploy:base-sepolia -- --json` dry-runs on chain id `84532`. +- Optional broadcast is approved and produces dated deployment facts. +- At least one Rootfield write emits FlowPulse on Base Sepolia. +- `npm run index:base-sepolia` persists state and checkpoint output. +- The checkpoint includes resume data and no secret material. +- Source verification is recorded or explicitly marked pending. +- `docs/CURRENT_STATE.md` is updated after any real broadcast. +- No production/mainnet/user-funds claim is made. diff --git a/docs/FLOWCHAIN_FULL_PRODUCT_L1_MASTER_PROMPTS.md b/docs/FLOWCHAIN_FULL_PRODUCT_L1_MASTER_PROMPTS.md index 6479b7c4..5fe29664 100644 --- a/docs/FLOWCHAIN_FULL_PRODUCT_L1_MASTER_PROMPTS.md +++ b/docs/FLOWCHAIN_FULL_PRODUCT_L1_MASTER_PROMPTS.md @@ -335,7 +335,7 @@ Forbidden folders: - `apps/dashboard/` except read-only coordination - `services/bridge-relayer/` -- production deployment scripts +- forbidden production deployment scripts Prompt: @@ -401,7 +401,7 @@ Forbidden folders: - runtime internals except documented handoff files - wallet private key handling -- production mainnet deployment +- forbidden production mainnet deployment Prompt: diff --git a/docs/FLOWCHAIN_OPERATOR_CHECKLIST.md b/docs/FLOWCHAIN_OPERATOR_CHECKLIST.md index 2a250c57..f1b7d69b 100644 --- a/docs/FLOWCHAIN_OPERATOR_CHECKLIST.md +++ b/docs/FLOWCHAIN_OPERATOR_CHECKLIST.md @@ -26,9 +26,10 @@ Check: - Whether `docs/FLOWCHAIN_TESTNET_ACCEPTANCE.md` has stale statuses. - Whether command names in `docs/FLOWCHAIN_SECOND_COMPUTER_SETUP.md` still match `package.json`. -- Whether any PR adds tokenomics, public validator onboarding, production - bridge work, production hook claims, audited-cryptography claims, or - production hardware claims. +- Whether any PR adds tokenomics, public validator onboarding, bridge behavior + outside the private/local package, hook behavior outside canary/test + surfaces, completed-cryptography-audit claims, or manufactured-hardware + claims. Second-computer readiness check: @@ -43,6 +44,87 @@ npm run flowchain:export Run `npm run flowchain:full-smoke` when the machine has the full prerequisite set, including Foundry, Python, dashboard dependencies, and crypto dependencies. +## Launch Demo Day + +Primary script: `docs/LAUNCH_DEMO_RUNBOOK.md`. + +Use this section when preparing a beginner-facing demo or review call. The demo +must stay inside the current local/private and guarded canary boundaries. + +Pre-demo gate: + +```powershell +cd E:\FlowMemory\flowmemory-main +git fetch --all --prune +git status --short --branch +npm run flowchain:prereq +npm run flowchain:full-smoke +``` + +If full smoke cannot run, record the exact missing prerequisite or first failing +command before proceeding. + +Generate demo state: + +```powershell +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +npm run flowchain:export +``` + +Start demo services in separate PowerShell windows: + +```powershell +npm run control-plane:serve +``` + +```powershell +npm run workbench:dev +``` + +Open or preload these routes: + +```text +http://127.0.0.1:5173/ +http://127.0.0.1:5173/overview +http://127.0.0.1:5173/flowmemory +http://127.0.0.1:5173/canary +http://127.0.0.1:5173/raw +``` + +Go/no-go checks: + +- `/` shows the Workbench. +- The banner says either **Local API detected.** or **Fixture fallback active.** + Both are valid if explained correctly. +- `/canary` shows Base canary review data and the not-production boundary. +- No private key, seed phrase, RPC credential, API key, or webhook URL is shown + on screen. +- The speaker says "private/local no-value validation" and "guarded Base + canary review," not production readiness. + +Allowed claims: + +- The current package has a local/private no-value demo path. +- The workbench renders generated V0 fixtures and can probe the local + control-plane API. +- The guarded Base canary route reviews committed reader output from known V0 + canary addresses. + +Blocked claims: + +- production readiness; +- mainnet readiness; +- public validators; +- tokenomics or real-funds workflows; +- production bridge readiness; +- production Uniswap v4 hook deployment; +- fully trustless verifier network; +- audited cryptography; +- AI/model/artifact data stored on-chain; +- production hardware. + ## During The Day - Keep each agent in its assigned worktree and folder lane. diff --git a/docs/FLOWCHAIN_TROUBLESHOOTING.md b/docs/FLOWCHAIN_TROUBLESHOOTING.md index a42884f5..cf5bb3b8 100644 --- a/docs/FLOWCHAIN_TROUBLESHOOTING.md +++ b/docs/FLOWCHAIN_TROUBLESHOOTING.md @@ -74,6 +74,86 @@ npm run flowchain:init npm run flowchain:demo ``` +## Workbench And Local Service Recovery + +The launch demo uses the dashboard as a local workbench. It can run with either +the local control-plane API or deterministic fixture fallback. + +### Workbench Loads With Fixture Fallback + +This is not automatically a failure. It means the browser did not detect the +local control-plane API at `http://127.0.0.1:8787`. + +To switch to local API mode, run in a separate PowerShell window: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm run control-plane:serve +``` + +Refresh the workbench. The banner should change from **Fixture fallback +active.** to **Local API detected.** + +### Control Plane Does Not Start + +Run the smallest local recovery path: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm install +npm run launch:v0 +npm test --prefix services/control-plane +npm run control-plane:serve +``` + +If port `8787` is already in use, inspect it: + +```powershell +Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinue +``` + +Close the old PowerShell window that owns the service, or stop the process if +you intentionally started it and no longer need it. + +### Workbench Dev Server Does Not Open + +Install dashboard dependencies and rerun the wrapper: + +```powershell +npm install --prefix apps/dashboard +npm run workbench:dev +``` + +Use the URL printed by Vite. It is usually `http://127.0.0.1:5173/`, but Vite +may choose another port if `5173` is busy. + +### Browser Shows Stale Or Missing Dashboard Data + +Refresh generated fixtures and sync the dashboard public copy: + +```powershell +npm run launch:v0 +npm run flowmemory:canary-dashboard +npm run sync:fixtures --prefix apps/dashboard +npm run workbench:dev +``` + +Do not refresh live canary data during a demo unless the operator explicitly +approves the RPC endpoint, canary addresses, and small block range. + +### Local State Looks Corrupt Or Confusing + +Reset ignored local state only: + +```powershell +npm run flowchain:stop -- -ResetLocalState +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +``` + +This does not edit committed fixtures. + ## Smoke Evidence After a successful smoke run, check: diff --git a/docs/INDEXER_VERIFIER_MVP.md b/docs/INDEXER_VERIFIER_MVP.md index 8dee9f48..3fbcd709 100644 --- a/docs/INDEXER_VERIFIER_MVP.md +++ b/docs/INDEXER_VERIFIER_MVP.md @@ -16,11 +16,15 @@ Run from the repository root: npm test npm run index:fixtures npm run index:base-sepolia -- --rpc-url --address --from-block --to-block +npm run index:base-sepolia -- --rpc-url --address --resume-from-checkpoint --to-block npm run verify:fixtures npm run e2e ``` -The fixture commands require no secrets and no live RPC. The Base Sepolia command requires an explicit RPC URL, refuses non-Base-Sepolia chain ids, and does not store the RPC URL in output artifacts. +The fixture commands require no secrets and no live RPC. The Base Sepolia +command requires an explicit resolved HTTP(S) RPC URL, refuses non-Base-Sepolia +chain ids, refuses broad scans by default, and does not store the RPC URL in +output artifacts. ## FlowPulse Input @@ -90,7 +94,8 @@ It requires: - `--rpc-url` - one or more `--address` values -- `--from-block` +- `--from-block`, or `--resume-from-checkpoint` when a checkpoint already + exists - `--to-block` It enforces: @@ -98,6 +103,9 @@ It enforces: - `eth_chainId` must be Base Sepolia (`84532`) - block values must be explicit decimal or `0x` quantities - emitting addresses must be explicit EVM addresses +- RPC URLs must be resolved HTTP(S) URLs without embedded username/password + credentials or obvious secret-shaped material +- scan spans must stay within the configured maximum block span - output files must not contain RPC URLs or private keys It writes: @@ -109,6 +117,10 @@ services/indexer/out/base-sepolia-indexer-checkpoint.json This is a testnet reader boundary, not a production mainnet indexer. +Checkpoint output includes `lastIndexedBlock`, `highestObservedBlock`, +`nextFromBlock`, and `emptyRange` so an operator can resume after empty or +non-empty reads without silently repeating the same window. + ## Identity Model `pulseId` is contract-emitted protocol data. It is not the canonical observed-log identity. @@ -235,7 +247,9 @@ Verifier report schema: flowmemory.verifier.report.v0 ``` -The JSON writer uses canonical key ordering, no wall-clock timestamps, no secrets, and stable fixture ordering. +The JSON writer uses canonical key ordering, no wall-clock timestamps in fixture +paths, no secrets, stable fixture ordering, and atomic temp-file replacement for +live reader state/checkpoint files. ## Off-Chain Boundary @@ -296,7 +310,8 @@ The control-plane exposes methods for health, chain status, node status, peers, What changed: - Added a runnable fixture-first indexer/verifier package with CLIs, persistence, schemas, and tests. -- Added a constrained Base Sepolia reader with durable local state and checkpoint output. +- Added a constrained Base Sepolia reader with durable local state, atomic + checkpoint output, resume support, and dashboard feed summaries. - Defined contract `pulseId`, indexer `observationId`, indexer `cursorId`, and verifier `reportId`. - Defined V0 lifecycle states, duplicate behavior, resolver policy boundaries, and report statuses. @@ -309,6 +324,7 @@ Checks: - `npm test` - `npm run index:fixtures` - `npm run index:base-sepolia -- --rpc-url --address --from-block --to-block ` +- `npm run index:base-sepolia -- --rpc-url --address --resume-from-checkpoint --to-block ` - `npm run verify:fixtures` - `npm run e2e` diff --git a/docs/LAUNCH_DEMO_RUNBOOK.md b/docs/LAUNCH_DEMO_RUNBOOK.md new file mode 100644 index 00000000..dadbd495 --- /dev/null +++ b/docs/LAUNCH_DEMO_RUNBOOK.md @@ -0,0 +1,532 @@ +# FlowMemory Launch Demo Runbook + +Status: beginner-safe launch demo script for the current V0 local/private and +guarded canary surfaces. + +This runbook is for showing what exists today without overclaiming. It covers: + +- the FlowChain private/local no-value workbench; +- the generated FlowMemory V0 dashboard fixtures; +- the guarded Base canary review route at `/canary`. + +It is not a production runbook, production mainnet announcement, token launch, +public validator guide, production verifier-network guide, production bridge +guide, or audited-cryptography statement. + +## One-Sentence Demo Frame + +Use this at the start: + +```text +This is FlowMemory V0: a local/private no-value FlowChain package and dashboard +workbench, plus a guarded Base canary review from known canary contracts. It +shows FlowPulse observations, Rootflow transitions, verifier/report state, and +operator evidence without claiming production readiness. +``` + +## What This Demo Proves + +- A beginner can run the current local package from the repo root. +- The local devnet can initialize deterministic no-value state and produce demo + output. +- The workbench can inspect local fixture state and, when started, the local + control-plane API. +- The canary route can review committed Base canary reader output separately + from local/private fixtures. +- The dashboard makes provenance, status, and boundaries visible instead of + hiding them in raw JSON. + +## What This Demo Does Not Prove + +Do not claim: + +- production readiness; +- mainnet readiness; +- a production L1; +- public validator readiness; +- tokenomics, token launch, or real-funds faucet behavior; +- production bridge readiness; +- production Uniswap v4 hook deployment; +- fully trustless verifier network; +- audited cryptography; +- AI, model weights, media, or heavy memory data stored on-chain; +- FlowRouter hardware manufacturing, certification, or field deployment. + +Safe language: + +- "local/private no-value validation"; +- "fixture-backed workbench"; +- "guarded Base canary review"; +- "FlowPulse observations"; +- "Rootflow transition model"; +- "commitments, receipts, roots, and verifier reports"; +- "source-verified current canary addresses, not production approval." + +## Pre-Demo Setup + +Run from the repo root: + +```powershell +cd E:\FlowMemory\flowmemory-main +git fetch --all --prune +git status --short --branch +npm install +npm install --prefix apps/dashboard +npm install --prefix crypto +npm run flowchain:prereq +``` + +Expected result: + +- `git status --short --branch` shows the intended branch and no unexpected + dirty files. +- `flowchain:prereq` lists Git, Node.js, npm, Rust/Cargo, MSVC build tools, + Foundry, and dependency install state. +- Missing dependencies are fixed before the demo starts. + +For a full launch-day gate, run: + +```powershell +npm run flowchain:full-smoke +``` + +Expected output files: + +```text +devnet/local/smoke/flowchain-smoke-report.json +devnet/local/full-smoke/flowchain-full-smoke-report.json +``` + +If full smoke cannot run on the demo computer, say exactly which prerequisite or +step is missing. Do not replace it with "works locally." + +## Generate Demo State + +Run: + +```powershell +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +npm run flowchain:export +``` + +What each command does: + +| Command | Meaning | +| --- | --- | +| `npm run flowchain:init` | Writes deterministic local state under `devnet/local/state.json` and a local-only operator file under `devnet/local/operator.local.json`. | +| `npm run flowchain:start` | Prepares launch-core fixtures, inspects local state, and writes bounded local stack status. This is not a daemon. | +| `npm run flowchain:demo` | Runs the deterministic no-value local demo and writes handoff output under `devnet/local/handoff/generated/`. | +| `npm run flowchain:export` | Writes local export files and a zip bundle under `devnet/local/export/` without including the local operator file. | + +## Start The Services For The Browser Demo + +Use two PowerShell windows. + +Window 1, optional but recommended: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm run control-plane:serve +``` + +Expected local API: + +```text +http://127.0.0.1:8787 +``` + +Window 2: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm run workbench:dev +``` + +Open the Vite URL printed by the command. It is usually: + +```text +http://127.0.0.1:5173/ +``` + +If Vite chooses a different port, use the printed URL. + +## Browser Click Script + +### 1. Workbench + +Open: + +```text +http://127.0.0.1:5173/ +``` + +Or click **Workbench** in the left nav. + +Say: + +```text +This is the local operator workbench. It checks the local control-plane API at +127.0.0.1:8787 and falls back to deterministic fixtures when the API is not +running. The fallback is intentional for demos; it is not pretending to be a +hosted production service. +``` + +Check the banner under the top bar: + +- **Local API detected.** The workbench is reading the local API where + available. +- **Fixture fallback active.** The control-plane service was not detected and + the workbench is showing deterministic committed fixtures. + +Main Workbench panels: + +| Panel | What it means | +| --- | --- | +| Node and API status | Current local chain/API health, block height, state root, pending transaction count, and API URL. If it says offline, the dashboard is still usable in fixture fallback mode. | +| Local setup path | The beginner command sequence the operator should run: install, launch candidate, start, smoke, and open workbench. | +| Control-plane endpoints and local actions | Endpoints advertised or probed from the local API. Browser action buttons only appear when the API advertises matching safe POST endpoints. | +| Workbench coverage metrics | Counts for data source, node views, chain objects, smoke objects, and open challenges in the current source. | +| Object switcher | Clickable object views for Node Status, Peers, Blocks, Transactions, Mempool, Accounts, Balances, Faucet Events, Wallet Metadata, Rootfields, Agents, Models, Work Receipts, Memory Cells, Artifacts, Verifier Modules, Verifier Reports, Challenges, Finality, bridge test objects, Provenance / Source, Hardware Signals, and Raw JSON. | +| Record grid | The records for the selected object view, including status, facts, and provenance. | +| Boundary notes | Reminder that this is one local workbench surface, not a second dashboard or production system. | + +What to click: + +1. Click **Node Status** in the object switcher and point out the state root, + chain id, API URL, and provenance. +2. Click **Blocks** and **Transactions** to show deterministic local chain + objects. +3. Click **Agents**, **Models**, **Work Receipts**, **Verifier Reports**, + **Memory Cells**, **Challenges**, and **Finality** to show the local object + lifecycle. +4. Click **Wallet Metadata** and say it is public metadata only. The browser + does not ask for private keys. +5. Click **Bridge Deposits**, **Bridge Credits**, and **Bridge Withdrawals** and + say these are private/local bridge-shaped test objects only. +6. Click **Provenance / Source** to show where the data came from. +7. Click **Raw JSON** only if a reviewer asks for the payload underneath the UI. + +If local action buttons appear, describe them as optional browser-safe local API +actions. Do not promise they will appear on every machine. + +### 2. Overview + +Click **Overview** or open: + +```text +http://127.0.0.1:5173/overview +``` + +Panels: + +| Panel | What it means | +| --- | --- | +| Metric tiles | Counts and status for the generated V0 fixture stack. | +| Recent FlowPulse observations | The latest observed FlowPulse-style records with receipt-derived transaction/log metadata. | +| Verifier attention | Reports that need review, or an empty state when fixture reports are verified. | +| Hardware risk | FlowRouter POC heartbeat records that are stale or risky in fixtures. Hardware is optional for this demo. | +| Devnet block window | Deterministic local block and state-root view. | + +Say: + +```text +FlowPulse is the observation spine. Contracts emit compact events; indexers and +verifiers derive receipt facts such as transaction hash and log index after the +receipt exists. +``` + +### 3. Flow Memory / Rootflow + +Click **Flow Memory** or open: + +```text +http://127.0.0.1:5173/flowmemory +``` + +Say: + +```text +This view turns observations, verifier reports, and roots into Flow Memory V0 +objects: MemorySignals, MemoryReceipts, RootfieldBundles, +AgentMemoryViews, and RootflowTransitions. +``` + +Use this view to show parent/child state transitions and status vocabulary: + +```text +observed, pending, finalized, verified, unresolved, failed, unsupported, +reorged, offline, stale +``` + +### 4. FlowPulse And Rootfields + +Click **FlowPulse**: + +```text +http://127.0.0.1:5173/flowpulse +``` + +Then click **Rootfields**: + +```text +http://127.0.0.1:5173/rootfields +``` + +Say: + +```text +Rootfields are namespaces and compact commitment state. They are not unlimited +storage, and metadata/evidence URIs are pointers or arbitrary log strings, not +raw model or artifact storage. +``` + +### 5. Work Lanes, Verifier, Devnet, Hardware, Alerts + +Click these only if time allows: + +| Route | What to show | +| --- | --- | +| `/work` | Work lanes and receipt status. | +| `/verifier` | Verifier reports and reason codes. These are local/test reports, not a production verifier network. | +| `/devnet` | Deterministic no-value local blocks and roots. | +| `/hardware` | FlowRouter POC heartbeat/control-signal records. This is not manufactured or field-deployed hardware. | +| `/alerts` | Operator warnings and recommended local actions. | + +### 6. Base Canary + +Click **Base canary** or open: + +```text +http://127.0.0.1:5173/canary +``` + +Say: + +```text +This route is intentionally separate from local fixture mode. It reviews a +guarded Base canary read over known V0 canary addresses and a small explicit +block range. It is visible on Base, but it is still a canary, not production +approval. +``` + +Current committed canary fixture summary: + +| Field | Current value | +| --- | --- | +| Chain id | `8453` | +| Read window | `45955500` to `45955540` | +| Canary FlowPulse observations | `4` | +| Rootfields | `1` | +| Rootflow transitions | `4` | +| Rejected logs | `0` | +| Duplicates | `0` | +| Contracts in canary artifact | `10` | + +Canary panels: + +| Panel | What it means | +| --- | --- | +| Canary boundary hero | The high-level warning: visible on Base, still gated for production. | +| Reader command / review fixture / hard boundary strip | The exact reader shape, runtime fixture path, and no broad scan/no production/no real-funds boundary. | +| Canary metrics | Counts from the committed canary fixture, including rejected logs and duplicates. | +| Canary FlowPulse stream | The observed canary logs with block, pulse type, transaction hash, and log index. | +| FlowPulse contracts | Canary contracts that emitted FlowPulse logs in the review fixture. | +| Launch gates | Boundaries that remain before any production claim. | +| Canary Rootflow state | Rootflow transitions reconstructed from the canary observations. | +| Canary JSON | Full loaded canary dashboard payload for reviewer inspection. | + +If someone asks how to refresh canary data, show the documented command but do +not run it unless the operator explicitly wants a live read and has approved the +RPC endpoint and block range: + +```powershell +npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url https://mainnet.base.org --address 0x2a7ADd68a1d45C3251E2F92fFe4926124654a97C --address 0x179Df6d52e9DeF5D02704583a2E4E5a9FF427245 --from-block 45955500 --to-block 45955540 --finalized-block 45955540 +npm run flowmemory:canary-dashboard +``` + +Boundary to say out loud: + +```text +The guarded reader refuses broad scans and marks checkpoint output as not +production-ready. The canary dashboard data is review evidence, not a +production service. +``` + +## Recovery During A Live Demo + +### Workbench Opens But Says Fixture Fallback + +This is acceptable. Say: + +```text +The local API is not detected, so the dashboard is showing deterministic +fixtures. That is an intended fallback path. +``` + +To recover live local API mode, start another PowerShell window: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm run control-plane:serve +``` + +Refresh the browser. The banner should change to **Local API detected.** + +### Control Plane Fails To Start + +Run: + +```powershell +cd E:\FlowMemory\flowmemory-main +npm install +npm run launch:v0 +npm test --prefix services/control-plane +npm run control-plane:serve +``` + +If the port is already in use: + +```powershell +Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinue +``` + +Close the old PowerShell window that owns the service, or stop that process if +you intentionally started it and no longer need it. + +### Workbench Does Not Open + +Run: + +```powershell +npm install --prefix apps/dashboard +npm run workbench:dev +``` + +Use the URL printed by Vite. If the browser is stale, refresh the page. If the +dev server is wedged, press `Ctrl+C` in the workbench window and rerun the +command. + +### Local State Looks Wrong + +Reset only ignored local devnet state: + +```powershell +npm run flowchain:stop -- -ResetLocalState +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +``` + +This does not edit committed fixtures. + +### Canary Route Does Not Load + +Refresh dashboard fixtures: + +```powershell +npm run flowmemory:canary-dashboard +npm run sync:fixtures --prefix apps/dashboard +npm run workbench:dev +``` + +If the canary JSON is stale, say it is the committed canary review fixture and +point to: + +```text +docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md +``` + +Do not invent a newer canary status. + +## Launch-Day Checklist + +### Before The Demo + +- Confirm the intended branch and clean state: + +```powershell +git fetch --all --prune +git status --short --branch +``` + +- Check GitHub state: + +```powershell +gh pr list --repo FlowmemoryAI/FlowMemory --state open +gh issue list --repo FlowmemoryAI/FlowMemory --state open --limit 80 +``` + +- Run the acceptance gate when prerequisites are present: + +```powershell +npm run flowchain:full-smoke +``` + +- Generate demo state: + +```powershell +npm run flowchain:init +npm run flowchain:start +npm run flowchain:demo +npm run flowchain:export +``` + +- Start local API and workbench in separate windows: + +```powershell +npm run control-plane:serve +``` + +```powershell +npm run workbench:dev +``` + +- Open these browser routes: + +```text +http://127.0.0.1:5173/ +http://127.0.0.1:5173/overview +http://127.0.0.1:5173/flowmemory +http://127.0.0.1:5173/canary +http://127.0.0.1:5173/raw +``` + +### During The Demo + +- Keep private keys, seed phrases, RPC credentials, API keys, and webhook URLs + off screen. +- Use the exact framing from this runbook. +- If a reviewer asks what is live, distinguish local/private runtime state, + deterministic fixtures, and committed canary reader output. +- If a reviewer asks whether it is production-ready, answer "no." +- If a reviewer asks whether the canary is on Base, answer "yes, as a guarded + V0 canary deployment for testing only." +- If a command fails, capture the first failing command and use + `docs/FLOWCHAIN_TROUBLESHOOTING.md`. + +### After The Demo + +Stop local windows with `Ctrl+C`, then record stopped state: + +```powershell +npm run flowchain:stop +``` + +Capture handoff evidence: + +```powershell +Get-Content -Raw devnet/local/smoke/flowchain-smoke-report.json +Get-Content -Raw devnet/local/full-smoke/flowchain-full-smoke-report.json +git status --short --branch +``` + +End the handoff with: + +- what was shown; +- commands run; +- whether full smoke passed or why it was skipped; +- any failure and first failing command; +- risks, assumptions, and follow-ups. + diff --git a/docs/MARKETING_CLAIMS_GUARDRAILS.md b/docs/MARKETING_CLAIMS_GUARDRAILS.md index e410aae1..b2bcd5e7 100644 --- a/docs/MARKETING_CLAIMS_GUARDRAILS.md +++ b/docs/MARKETING_CLAIMS_GUARDRAILS.md @@ -12,13 +12,24 @@ FlowMemory can have ambitious language. It should not make claims that the curre - Flow Memory V0 exposes agent-facing MemorySignal, MemoryReceipt, RootfieldBundle, AgentMemoryView, and RootflowTransition objects. - Heavy AI, model, memory, media, and artifact data stays off-chain while commitments and receipts stay on-chain or in signed fixtures. - Base Sepolia live reading is being built as a testnet reader path. +- The documented Base mainnet V0 canary can be described only as a guarded + canary/testing deployment with `productionReady: false`. - FlowRouter and FlowNet are research directions for hardware, cache, mesh signaling, and receipt relay. ## Claims That Remain Blocked - FlowMemory is production-ready. - FlowMemory is mainnet-ready. +- FlowMemory has had a production launch. +- FlowMemory has had a mainnet launch. +- FlowMemory has production mainnet contracts. +- FlowMemory has production deployment automation. - FlowMemory is a production L1. +- FlowMemory runs a production verifier network. +- FlowMemory has a production Uniswap v4 hook. +- FlowMemory has a production bridge. +- FlowMemory provides production custody or wallet support. +- FlowMemory cryptography or contracts are audited unless a named audit artifact exists. - AI runs on-chain. - Storage is free. - Transaction hashes store arbitrary AI data. @@ -35,6 +46,8 @@ Use: - local/test V0 - fixture-backed dashboard - Base Sepolia reader path +- guarded Base mainnet canary for documented V0 testing only +- canary-only and `productionReady: false` - off-chain verification path - commitments, receipts, roots, and state transitions - future appchain/L1 research @@ -43,7 +56,12 @@ Avoid: - production launch - mainnet launch +- production mainnet contracts - trustless network +- production verifier network +- production bridge +- production custody +- audited cryptography - on-chain AI - free storage - decentralized ISP replacement diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ea1c7c86..944f41c6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -94,11 +94,11 @@ Status: implemented as fixture-first services plus generated launch-core state; - Generated MemorySignal and RootflowTransition fixtures expose contract-event linkage through `contractEvent` and `contractEventRef`. - Fixture-based parser and reorg-state tests exist in the indexer/verifier packages. - Deterministic persistence exists for fixture state, the constrained Base Sepolia reader checkpoint, and the guarded Base mainnet canary checkpoint. -- A Base Sepolia reader path exists for explicit RPC URLs and explicit FlowPulse contract addresses; it rejects non-Base-Sepolia chain ids. +- A Base Sepolia reader path exists for explicit RPC URLs and explicit FlowPulse contract addresses; it rejects non-Base-Sepolia chain ids, refuses broad scans by default, persists atomic checkpoint output, and supports resume from `nextFromBlock`. - A guarded Base mainnet canary reader exists for explicit RPC URLs, explicit known canary addresses, and small explicit block ranges; it rejects non-Base-mainnet chain ids and marks output as canary-only. - A separate dashboard canary mode exists for committed Base canary reader output and deployment artifacts. - Source verification automation exists for the canary contract set, and all current Base canary addresses are verified on BaseScan. Future redeploys still require a local `BASESCAN_API_KEY` for submission. -- Base Sepolia deploy/read commands exist for the current V0 testnet contract set. +- Base Sepolia plan/dry-run/broadcast/read commands and a rehearsal runbook exist for the current V0 public testnet contract set. - A Base mainnet V0 canary deployment has been performed for testing only and is documented under `docs/DEPLOYMENTS/`. - Runtime schema validation and generated fixture drift checks exist for launch-core outputs. - Local devnet smoke-test gates exist as a no-value Rust prototype, without mainnet or production deployment. @@ -126,6 +126,7 @@ Status: active. - Add security reporting guidance. - Enforce claim guardrails in CI for README/docs/marketing surfaces. - Keep Slither required for audit environments and available through `npm run contracts:hardening:slither`. +- Keep `docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md` current when launch-facing contract, reader, dashboard, wallet, or claim surfaces change. - Enforce allowed-folder and forbidden-folder boundaries. ## Mid-Term Phases @@ -197,7 +198,7 @@ Next merge preference: 7. Contracts settlement-spine alignment without moving runtime into Solidity. 8. Refresh packaging scripts and root command aliases whenever subsystem command semantics change. 9. Canary ingestion and Base Sepolia follow-ups, still gated from production claims. -10. Base Sepolia reader soak tests against explicit testnet deployments. +10. Base Sepolia rehearsal execution against explicit testnet deployments. 11. Actual source verification submission for Base canary contracts when `BASESCAN_API_KEY` is available. 12. Static analysis follow-up findings triaged for any public testnet deployment. 13. Operator ownership separation and multisig/recovery decision before further live deployments. diff --git a/docs/SECURITY_MODEL.md b/docs/SECURITY_MODEL.md index 820bd741..c1554a9c 100644 --- a/docs/SECURITY_MODEL.md +++ b/docs/SECURITY_MODEL.md @@ -161,6 +161,31 @@ operator separation, multisig or comparable account-control decisions, key-rotation/recovery, verifier signing policy, PoolManager hook wiring, and a recorded go/no-go decision are required before any production claim. +### Launch-Candidate Boundary Review + +The 2026-05-13 launch-candidate security boundary review lives in +`docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md`. + +Current security conclusion: + +- Local/test V0 demos and guarded Base canary review are acceptable with + explicit canary-only framing. +- No production or mainnet-launch claim is approved. +- Direct single-account ownership, owner allowlists, self-registration, and open + submission are V0 coordination surfaces only. +- `BASE_SEPOLIA_DEPLOYER_KEY_HEX` is read from the local environment for the + Base Sepolia deploy wrapper, but the wrapper passes it to Foundry as a + command-line private key. That is a local/test caveat and a blocker for any + future production signing path. +- The local encrypted test vault is no-value private/local tooling, not + production custody or audited key management. +- The guarded Base mainnet canary reader is for documented canary addresses and + bounded block ranges only. It is not a broad production indexer. +- `FlowMemoryAfterSwapHook` is a production-shaped hook candidate, not a + production Uniswap v4 hook deployment. Any future hook broadcast requires a + recorded salt/address, permission-bit review, PoolManager integration review, + source-verification plan, post-deploy read range, and go/no-go decision. + ## PR Security Checklist - Does this change introduce or require secrets? diff --git a/docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md b/docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md new file mode 100644 index 00000000..9551120c --- /dev/null +++ b/docs/reviews/LAUNCH_CANDIDATE_SECURITY_BOUNDARY_REVIEW.md @@ -0,0 +1,115 @@ +# Launch-Candidate Security Boundary Review + +Date: 2026-05-13 + +Reviewer role: Security Review Agent + +Result: pass for local/test V0 launch-candidate demos and guarded canary review. +Result: fail for any production, mainnet-launch, custody, verifier-network, +bridge, audited-cryptography, or production Uniswap hook claim. + +## Scope + +Reviewed the launch-candidate boundary across: + +- source-of-truth docs: `docs/START_HERE.md`, + `docs/FLOWMEMORY_HQ_CONTEXT.md`, `docs/CURRENT_STATE.md`, + `docs/SECURITY_MODEL.md`, `docs/MARKETING_CLAIMS_GUARDRAILS.md`, + `docs/PRODUCTION_READINESS_CHECKLIST.md`, and deployment/operator docs; +- existing review docs under `docs/reviews/`; +- contract boundary docs and selected contract surfaces for access-control + assumptions; +- deployment, reader, canary, local wallet, and smoke scripts; +- README and launch-facing claim surfaces. + +GitHub/source check: local `HEAD` and `origin/HEAD` both resolved to +`98386b9e40c271c5bac2d41ba3a9010d9e9da36e` before edits. + +## Pass/Fail Matrix + +| Area | V0 result | Notes | +| --- | --- | --- | +| Source-of-truth consistency | Pass with caveat | Current docs consistently say V0/local/test, with a real Base mainnet canary documented as canary-only. README still has older "do not claim mainnet deployment" wording that should be clarified to "production mainnet deployment" in a later README-scoped task. | +| Access control | Pass for V0, fail for production | Per-record owners, owner allowlists, self-registration, and open submission surfaces are documented. Direct deployer ownership, no multisig, no recovery, no timelock, and no decentralized verifier governance remain production blockers. | +| Unsafe claims | Pass after guardrail expansion | Claim scanning now covers README, docs, contract docs, and marketing, and blocks more production/mainnet launch wording unless explicitly framed as blocked, not implemented, or out of scope. | +| Secret handling | Pass for committed V0 surfaces, fail for production signing | `.gitignore`, CI secret checks, control-plane no-secret scanning, and canary reader outputs avoid committed credentials. The Base Sepolia deploy wrapper still passes a private key to `forge --private-key`, which is acceptable only as a local/test operator caveat. | +| Local wallet boundary | Pass for local no-value tests | The encrypted local test vault excludes private keys from public exports and is exercised in ignored `devnet/local/` output. It is not production custody, wallet connect, or value-bearing key management. | +| Deployment scripts | Pass for dry-run/testnet/canary boundaries | Base Sepolia deploy requires explicit env inputs. Base canary reading requires explicit acknowledgement, addresses, and small block ranges. Source verification redacts API key material. | +| Base mainnet canary | Pass as canary-only | The documented canary is real Base mainnet activity, but artifacts and dashboard state mark `productionReady: false` and separate canary data from local fixture acceptance. | +| Uniswap hook assumptions | Pass for V0, fail for production hook readiness | `FlowMemoryHookAdapter` is open/canary scaffold. `FlowMemoryAfterSwapHook` is PoolManager-gated and afterSwap-only, but there is no recorded mined hook address, deployment, PoolManager integration, source-verification plan, or go/no-go approval for production. | +| Verifier/trust claims | Pass for local signed statements | V0 verifier reports and attestations are deterministic signed claims. They are not zk proofs, decentralized consensus, staking, slashing, or a production verifier network. | +| Bridge and real funds | Pass for POC/canary-read boundary, fail for production bridge | `BaseBridgeLockbox` is test-only, owner-controlled, capped, and not trustless. Mainnet canary reads require real-funds acknowledgement and a small USD cap, but releases remain owner-controlled. | +| URI/off-chain data boundary | Pass as documented caveat | `metadataURI` and `evidenceURI` are arbitrary log strings. Contracts do not enforce length, content type, resolvability, privacy, or short-pointer behavior. | + +## Blockers Before Any Production Claim + +These are launch blockers for production language, not blockers for local/test +V0 demos: + +- Record an explicit go/no-go decision in `docs/DECISIONS/`. +- Replace direct single-account operator ownership with a reviewed multisig or + comparable account-control policy, including rotation and recovery. +- Separate deployer, worker admin, verifier admin, and emergency-response + authority. +- Stop using command-line private-key broadcast for any production signing path; + require a safer signer or keystore/hardware-wallet flow. +- Define verifier identity, signing policy, challenge lifecycle, finality, + replay policy, and verifier-set governance. +- Complete and record source verification for every future deployed or + redeployed contract address. +- For Uniswap v4, record the mined hook salt/address, hook permission bits, + PoolManager address, constructor args, init code hash, deployment block, + post-deploy reader range, and integration review. +- Keep broad Base mainnet readers disabled unless a separate production indexer + design and incident-response plan are accepted. +- Add URI/pointer policy if any public copy implies off-chain storage + enforcement, content availability, or privacy. +- Keep bridge releases, real-funds flows, and custody out of public user paths + until a bridge threat model, release policy, caps, monitoring, and emergency + playbook are accepted. +- Do not describe local wallet helpers as production custody or wallet support. +- Do not describe current verifier reports as audited or fully trustless proofs. + +## Acceptable V0 Caveats + +The following are acceptable only with explicit local/test/canary framing: + +- Direct owner and owner-allowlist contracts for V0 commitment surfaces. +- Self-registration registries when downstream docs treat registrations as + untrusted until verifier reports exist. +- Open submission surfaces that emit reconstructable commitments but do not + custody funds or claim correctness. +- Base Sepolia deployment dry runs and explicit broadcasts using local ignored + environment variables. +- Guarded Base mainnet canary reads for documented canary addresses and small + explicit block ranges. +- A Base mainnet canary deployment described as canary-only and + `productionReady: false`. +- Local encrypted no-value test vaults stored under ignored local output paths. +- Bridge POC observations and canary reads that do not claim production bridge + security or real user deposit readiness. +- Fixture-backed dashboard and canary dashboard modes that stay visually and + semantically separate. + +## Concrete Guardrail Added + +`infra/scripts/check-unsafe-claims.mjs` now: + +- scans `contracts/` markdown boundary docs in addition to `README.md`, + `docs/`, and `marketing/`; +- blocks unguarded positive claims for production launch, mainnet launch, + production-mainnet, production deployment, production verifier network, + production Uniswap hook, production bridge, production custody, and audited + status; +- keeps existing allowances for lines or sections that clearly mark those + phrases as blocked, rejected, gated, not implemented, or out of scope. + +## Review Decision + +No production or mainnet-launch claim is approved. + +FlowMemory can proceed with V0 launch-candidate demos only if copy continues to +say local/test, Base Sepolia, guarded Base mainnet canary, no-value devnet, +fixture-backed dashboard, canary-only, and `productionReady: false` where +applicable. + diff --git a/fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json b/fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json index d6ac2de2..e4e7c87d 100644 --- a/fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json +++ b/fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json @@ -1,7 +1,7 @@ { "metadata": { "schema": "flowmemory.dashboard.fixture.v0", - "generatedAt": "2026-05-13T20:05:51.625Z", + "generatedAt": "2026-05-13T23:49:07.586Z", "mode": "canary", "description": "Generated Base mainnet canary dashboard data from the guarded FlowPulse reader. It is canary-only and not a production-readiness claim.", "fixturePath": "fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json", @@ -136,7 +136,7 @@ "currentBlock": 45955540, "finalizedBlock": 45955540, "source": "live", - "lastUpdated": "2026-05-13T20:05:51.625Z" + "lastUpdated": "2026-05-13T23:49:07.586Z" }, "flowPulseObservations": [ { @@ -161,13 +161,13 @@ "uri": "flowmemory://base-canary/rootfield", "summary": "rootfield registration from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -193,13 +193,13 @@ "uri": "flowmemory://uniswap-v4/after-swap", "summary": "swap memory signal from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -225,13 +225,13 @@ "uri": "flowmemory://base-canary/root", "summary": "root commitment from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -257,13 +257,13 @@ "uri": "flowmemory://uniswap-v4/after-swap", "summary": "swap memory signal from Base canary reader", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -284,13 +284,13 @@ ], "evidenceUri": "docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md", "status": "finalized", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -331,13 +331,13 @@ "logIndex": "229" }, "id": "0xef6e3d4a6375fdaf3573db4550b7a6c4ac535f6ddbf8a2759014c7e6ee5dc17d", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -373,13 +373,13 @@ "logIndex": "274" }, "id": "0x7da37f62f68f29fdd92dde90fdb5783bce555c868bfb0181187ae955eb2d8c95", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -415,13 +415,13 @@ "logIndex": "364" }, "id": "0x508409804fe0dae77e8ced57cb45968877fcbad6f525a99b4f911ce58759eab7", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -457,13 +457,13 @@ "logIndex": "1212" }, "id": "0x91d0da9bc41fad8cb3fa66b72ba804f6685d34f0c167bd3fbf1ac48f91402188", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -534,13 +534,13 @@ ] }, "id": "0x1d530aa927338513f49fbd12145d05b1428347b5cfa0fb241033a9d070123637", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -609,13 +609,13 @@ ] }, "id": "0x4dbf295239f26095033ddbc39dbb3d2780a554ca697d3ea89c588bda449939d1", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -684,13 +684,13 @@ ] }, "id": "0x79640fe5901417b9254f9de76f011cdd54ffe9587aaf564f241f4b0cc2ec82da", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } }, @@ -759,13 +759,13 @@ ] }, "id": "0x623c146bc8899c5edadaed215cc86a1292f1f5c97acb245e86c0462190078ca5", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -804,13 +804,13 @@ "unsupported": 0, "reorged": 0 }, - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "indexer", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -842,13 +842,13 @@ "Source verification and operator policy must be completed before any production claim." ], "localOnly": false, - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "worker", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-indexer-state.json" } } @@ -862,7 +862,7 @@ "severity": "info", "title": "Canary mode", "summary": "Base mainnet canary logs are visible, but verifier reports, source verification, multisig ownership, and production hook wiring are not complete.", - "openedAt": "2026-05-13T20:05:51.625Z", + "openedAt": "2026-05-13T23:49:07.586Z", "linkedObjectIds": [ "0x2a7ADd68a1d45C3251E2F92fFe4926124654a97C", "0x179Df6d52e9DeF5D02704583a2E4E5a9FF427245", @@ -877,13 +877,13 @@ ], "recommendedAction": "Use this view for launch demonstrations and operator review only.", "status": "unresolved", - "lastUpdated": "2026-05-13T20:05:51.625Z", + "lastUpdated": "2026-05-13T23:49:07.586Z", "provenance": { "subsystem": "alerts", "origin": "live", "chainContext": "base-mainnet-canary", "fixturePath": "fixtures/deployments/base-canary-indexer-state.json", - "capturedAt": "2026-05-13T20:05:51.625Z", + "capturedAt": "2026-05-13T23:49:07.586Z", "localPathHint": "fixtures/deployments/base-canary-v0.json" } } diff --git a/fixtures/deployments/base-canary-indexer-checkpoint.json b/fixtures/deployments/base-canary-indexer-checkpoint.json index 00fcd6f1..c818969a 100644 --- a/fixtures/deployments/base-canary-indexer-checkpoint.json +++ b/fixtures/deployments/base-canary-indexer-checkpoint.json @@ -1 +1 @@ -{"addresses":["0x179df6d52e9def5d02704583a2e4e5a9ff427245","0x2a7add68a1d45c3251e2f92ffe4926124654a97c"],"chainId":"8453","cursorCount":4,"duplicateCount":0,"finalizedBlockNumber":"45955540","fromBlock":"45955500","generatedAt":"2026-05-13T20:05:51.625Z","lastIndexedBlock":"45955535","network":"base-mainnet-canary","observationCount":4,"rejectedLogCount":0,"safety":{"acknowledgement":"base-mainnet-canary-only","productionReady":false},"schema":"flowmemory.indexer.base_canary_checkpoint.v0","source":"base-mainnet-canary-rpc","statePath":"E:\\FlowMemory\\flowmemory-main\\fixtures\\deployments\\base-canary-indexer-state.json","toBlock":"45955540"} +{"addresses":["0x179df6d52e9def5d02704583a2e4e5a9ff427245","0x2a7add68a1d45c3251e2f92ffe4926124654a97c"],"chainId":"8453","cursorCount":4,"dashboardFeed":{"dashboardCanonicalObservationCount":4,"duplicateCount":0,"duplicateKindCounts":{},"feedSchema":"flowmemory.indexer.dashboard_feed.v0","hasIntegrityWarnings":false,"observationCount":4,"rejectedLogCount":0,"rejectedReasonCounts":{},"schema":"flowmemory.indexer.checkpoint_dashboard_feed.v0","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","warningCodes":[]},"duplicateCount":0,"emptyRange":false,"finalizedBlockNumber":"45955540","fromBlock":"45955500","generatedAt":"2026-05-13T23:49:07.586Z","highestObservedBlock":"45955535","lastIndexedBlock":"45955540","lastScannedBlock":"45955540","network":"base-mainnet-canary","nextFromBlock":"45955541","observationCount":4,"rejectedLogCount":0,"safety":{"acknowledgement":"base-mainnet-canary-only","productionReady":false,"storesPrivateKeys":false,"storesRpcUrl":false},"schema":"flowmemory.indexer.base_canary_checkpoint.v0","source":"base-mainnet-canary-rpc","stateDigest":"0x2351d7406c0f856d2c340de7fea4b099f0511c1100b43e1bf567fdb58e54975d","statePath":"E:\\FlowMemory\\flowmemory-main\\fixtures\\deployments\\base-canary-indexer-state.json","toBlock":"45955540"} diff --git a/fixtures/deployments/base-canary-indexer-state.json b/fixtures/deployments/base-canary-indexer-state.json index 7526f8eb..78c93391 100644 --- a/fixtures/deployments/base-canary-indexer-state.json +++ b/fixtures/deployments/base-canary-indexer-state.json @@ -1 +1 @@ -{"schema":"flowmemory.indexer.persistence.v0","state":{"batches":[{"cursorCount":4,"observationCount":4,"rejectedLogCount":0,"schema":"flowmemory.indexer.batch.v0","source":"base-mainnet-canary-rpc","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef"}],"cursors":[{"blockHash":"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758","blockNumber":"45955506","chainId":"8453","cursorId":"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440","logIndex":"229","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"37"},{"blockHash":"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c","blockNumber":"45955535","chainId":"8453","cursorId":"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba","logIndex":"1212","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"194"},{"blockHash":"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52","blockNumber":"45955533","chainId":"8453","cursorId":"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2","logIndex":"364","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"131"},{"blockHash":"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336","blockNumber":"45955507","chainId":"8453","cursorId":"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7","logIndex":"274","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"74"}],"duplicates":[],"observations":[{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758","blockNumber":"45955506","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758\",\"blockNumber\":\"45955506\",\"chainId\":\"8453\",\"commitment\":\"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd\",\"cursorId\":\"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440\",\"emittingContract\":\"0x2a7add68a1d45c3251e2f92ffe4926124654a97c\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"229\",\"observationId\":\"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9\",\"occurredAt\":\"1778700359\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"1\",\"subject\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"transactionIndex\":\"37\",\"txHash\":\"0x994b98b1cff0c897d75b62cf7c95340f74a59d0c208af68f1dea2d161b80cf00\",\"uri\":\"flowmemory://base-canary/rootfield\"}","chainId":"8453","commitment":"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd","cursorId":"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440","duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"229","observationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","occurredAt":"1778700359","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseType":"1","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","transactionIndex":"37","txHash":"0x994b98b1cff0c897d75b62cf7c95340f74a59d0c208af68f1dea2d161b80cf00","uri":"flowmemory://base-canary/rootfield"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336","blockNumber":"45955507","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336\",\"blockNumber\":\"45955507\",\"chainId\":\"8453\",\"commitment\":\"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9\",\"cursorId\":\"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7\",\"emittingContract\":\"0x179df6d52e9def5d02704583a2e4e5a9ff427245\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"274\",\"observationId\":\"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68\",\"occurredAt\":\"1778700361\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"1\",\"subject\":\"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434\",\"transactionIndex\":\"74\",\"txHash\":\"0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7\",\"uri\":\"flowmemory://uniswap-v4/after-swap\"}","chainId":"8453","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","cursorId":"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7","duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"274","observationId":"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68","occurredAt":"1778700361","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7","pulseType":"4","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","transactionIndex":"74","txHash":"0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7","uri":"flowmemory://uniswap-v4/after-swap"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52","blockNumber":"45955533","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52\",\"blockNumber\":\"45955533\",\"chainId\":\"8453\",\"commitment\":\"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039\",\"cursorId\":\"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2\",\"emittingContract\":\"0x2a7add68a1d45c3251e2f92ffe4926124654a97c\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"364\",\"observationId\":\"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d\",\"occurredAt\":\"1778700413\",\"parentPulseId\":\"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87\",\"pulseId\":\"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"2\",\"subject\":\"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3\",\"transactionIndex\":\"131\",\"txHash\":\"0x24a43789ef489dd6c697567466944a210273e46c333e7be878cda6df9acb8e7a\",\"uri\":\"flowmemory://base-canary/root\"}","chainId":"8453","commitment":"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039","cursorId":"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2","duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"364","observationId":"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d","occurredAt":"1778700413","parentPulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseId":"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab","pulseType":"2","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3","transactionIndex":"131","txHash":"0x24a43789ef489dd6c697567466944a210273e46c333e7be878cda6df9acb8e7a","uri":"flowmemory://base-canary/root"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c","blockNumber":"45955535","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c\",\"blockNumber\":\"45955535\",\"chainId\":\"8453\",\"commitment\":\"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9\",\"cursorId\":\"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba\",\"emittingContract\":\"0x179df6d52e9def5d02704583a2e4e5a9ff427245\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"1212\",\"observationId\":\"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6\",\"occurredAt\":\"1778700417\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"2\",\"subject\":\"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434\",\"transactionIndex\":\"194\",\"txHash\":\"0xaee21f6d0e9df1a45eae0c7714a4f8eae7fb72afbb07dd67b3a1f0ff724a014f\",\"uri\":\"flowmemory://uniswap-v4/after-swap\"}","chainId":"8453","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","cursorId":"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba","duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"1212","observationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","occurredAt":"1778700417","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591","pulseType":"4","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","transactionIndex":"194","txHash":"0xaee21f6d0e9df1a45eae0c7714a4f8eae7fb72afbb07dd67b3a1f0ff724a014f","uri":"flowmemory://uniswap-v4/after-swap"}],"pulses":[{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd","observationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","occurredAt":"1778700359","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseType":"1","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","uri":"flowmemory://base-canary/rootfield"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","observationId":"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68","occurredAt":"1778700361","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","uri":"flowmemory://uniswap-v4/after-swap"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039","observationId":"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d","occurredAt":"1778700413","parentPulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseId":"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab","pulseType":"2","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3","uri":"flowmemory://base-canary/root"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","observationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","occurredAt":"1778700417","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","uri":"flowmemory://uniswap-v4/after-swap"}],"rejectedLogs":[],"rootfields":[{"firstObservationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","latestObservationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","pulseCount":4,"rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114"}],"schema":"flowmemory.indexer.state.v0","source":"base-mainnet-canary-rpc"}} +{"schema":"flowmemory.indexer.persistence.v0","state":{"batches":[{"cursorCount":4,"observationCount":4,"rejectedLogCount":0,"schema":"flowmemory.indexer.batch.v0","source":"base-mainnet-canary-rpc","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef"}],"cursors":[{"blockHash":"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758","blockNumber":"45955506","chainId":"8453","cursorId":"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440","logIndex":"229","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"37"},{"blockHash":"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c","blockNumber":"45955535","chainId":"8453","cursorId":"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba","logIndex":"1212","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"194"},{"blockHash":"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52","blockNumber":"45955533","chainId":"8453","cursorId":"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2","logIndex":"364","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"131"},{"blockHash":"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336","blockNumber":"45955507","chainId":"8453","cursorId":"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7","logIndex":"274","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","transactionIndex":"74"}],"dashboardFeed":{"chainId":"8453","dashboardCanonicalObservationCount":4,"duplicateCount":0,"duplicateKindCounts":{},"hasIntegrityWarnings":false,"observationCount":4,"observations":[{"blockHash":"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758","blockNumber":"45955506","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","lifecycleState":"finalized","logIndex":"229","observationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","occurredAt":"1778700359","pulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseType":"1","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","transactionIndex":"37","txHash":"0x994b98b1cff0c897d75b62cf7c95340f74a59d0c208af68f1dea2d161b80cf00","uri":"flowmemory://base-canary/rootfield"},{"blockHash":"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336","blockNumber":"45955507","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","lifecycleState":"finalized","logIndex":"274","observationId":"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68","occurredAt":"1778700361","pulseId":"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","transactionIndex":"74","txHash":"0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7","uri":"flowmemory://uniswap-v4/after-swap"},{"blockHash":"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52","blockNumber":"45955533","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","lifecycleState":"finalized","logIndex":"364","observationId":"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d","occurredAt":"1778700413","pulseId":"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab","pulseType":"2","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","transactionIndex":"131","txHash":"0x24a43789ef489dd6c697567466944a210273e46c333e7be878cda6df9acb8e7a","uri":"flowmemory://base-canary/root"},{"blockHash":"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c","blockNumber":"45955535","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","lifecycleState":"finalized","logIndex":"1212","observationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","occurredAt":"1778700417","pulseId":"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","transactionIndex":"194","txHash":"0xaee21f6d0e9df1a45eae0c7714a4f8eae7fb72afbb07dd67b3a1f0ff724a014f","uri":"flowmemory://uniswap-v4/after-swap"}],"rejectedLogCount":0,"rejectedReasonCounts":{},"schema":"flowmemory.indexer.dashboard_feed.v0","source":"base-mainnet-canary-rpc","sourceSetId":"0xef612a85cb90730538fc4862ef9375c4a7b2d5318745f0b36a0248088d5fcaef","warningCodes":[]},"duplicates":[],"observations":[{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758","blockNumber":"45955506","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0xb99c82844b5ea00482b5e36724e167dbaa3276e872de6884c736f715b6acc758\",\"blockNumber\":\"45955506\",\"chainId\":\"8453\",\"commitment\":\"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd\",\"cursorId\":\"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440\",\"emittingContract\":\"0x2a7add68a1d45c3251e2f92ffe4926124654a97c\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"229\",\"observationId\":\"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9\",\"occurredAt\":\"1778700359\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"1\",\"subject\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"transactionIndex\":\"37\",\"txHash\":\"0x994b98b1cff0c897d75b62cf7c95340f74a59d0c208af68f1dea2d161b80cf00\",\"uri\":\"flowmemory://base-canary/rootfield\"}","chainId":"8453","commitment":"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd","cursorId":"0x3651ab56bfc86d78628f5cda2e02e363eadb68aa986d38c8e6f2bd938b983440","duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"229","observationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","occurredAt":"1778700359","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseType":"1","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","transactionIndex":"37","txHash":"0x994b98b1cff0c897d75b62cf7c95340f74a59d0c208af68f1dea2d161b80cf00","uri":"flowmemory://base-canary/rootfield"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336","blockNumber":"45955507","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x80c8eaffc3d5ebaa3eda56ba83ea5fefa1c9c37777dde659c0204c0a04dbe336\",\"blockNumber\":\"45955507\",\"chainId\":\"8453\",\"commitment\":\"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9\",\"cursorId\":\"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7\",\"emittingContract\":\"0x179df6d52e9def5d02704583a2e4e5a9ff427245\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"274\",\"observationId\":\"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68\",\"occurredAt\":\"1778700361\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"1\",\"subject\":\"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434\",\"transactionIndex\":\"74\",\"txHash\":\"0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7\",\"uri\":\"flowmemory://uniswap-v4/after-swap\"}","chainId":"8453","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","cursorId":"0xac5f50cd599ff8fb84ef784bc33bd6eb766c48fbd2ec53348cc8092c20fb60e7","duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"274","observationId":"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68","occurredAt":"1778700361","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7","pulseType":"4","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","transactionIndex":"74","txHash":"0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7","uri":"flowmemory://uniswap-v4/after-swap"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52","blockNumber":"45955533","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x7fc3c3aa61c897fc71fdf7d065a1bda685442cb2096c546d2d1dba4da3339b52\",\"blockNumber\":\"45955533\",\"chainId\":\"8453\",\"commitment\":\"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039\",\"cursorId\":\"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2\",\"emittingContract\":\"0x2a7add68a1d45c3251e2f92ffe4926124654a97c\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"364\",\"observationId\":\"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d\",\"occurredAt\":\"1778700413\",\"parentPulseId\":\"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87\",\"pulseId\":\"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"2\",\"subject\":\"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3\",\"transactionIndex\":\"131\",\"txHash\":\"0x24a43789ef489dd6c697567466944a210273e46c333e7be878cda6df9acb8e7a\",\"uri\":\"flowmemory://base-canary/root\"}","chainId":"8453","commitment":"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039","cursorId":"0x4f03451e8f8f4bda65a8788e0753697d6beb3f5ebc3574c0cd3da5139ef984e2","duplicateKind":"unique","emittingContract":"0x2a7add68a1d45c3251e2f92ffe4926124654a97c","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"364","observationId":"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d","occurredAt":"1778700413","parentPulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseId":"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab","pulseType":"2","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3","transactionIndex":"131","txHash":"0x24a43789ef489dd6c697567466944a210273e46c333e7be878cda6df9acb8e7a","uri":"flowmemory://base-canary/root"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","blockHash":"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c","blockNumber":"45955535","canonicalObservationJson":"{\"actor\":\"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9\",\"blockHash\":\"0x0510d6a627884b7b3677a7223a2720d1e33f5f4e58f2c0c6c7baf27bcec25d7c\",\"blockNumber\":\"45955535\",\"chainId\":\"8453\",\"commitment\":\"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9\",\"cursorId\":\"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba\",\"emittingContract\":\"0x179df6d52e9def5d02704583a2e4e5a9ff427245\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"1212\",\"observationId\":\"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6\",\"occurredAt\":\"1778700417\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114\",\"sequence\":\"2\",\"subject\":\"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434\",\"transactionIndex\":\"194\",\"txHash\":\"0xaee21f6d0e9df1a45eae0c7714a4f8eae7fb72afbb07dd67b3a1f0ff724a014f\",\"uri\":\"flowmemory://uniswap-v4/after-swap\"}","chainId":"8453","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","cursorId":"0x49b89d1e961522eac1b018fc62339da5a0f8a3152bde6055417e31eb9adf39ba","duplicateKind":"unique","emittingContract":"0x179df6d52e9def5d02704583a2e4e5a9ff427245","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"1212","observationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","occurredAt":"1778700417","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591","pulseType":"4","receiptStatus":"success","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","transactionIndex":"194","txHash":"0xaee21f6d0e9df1a45eae0c7714a4f8eae7fb72afbb07dd67b3a1f0ff724a014f","uri":"flowmemory://uniswap-v4/after-swap"}],"pulses":[{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x421370a88b0e8632d6a90aeffa276eda7dd5995bb18cb7ce0fb5d36a553e98dd","observationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","occurredAt":"1778700359","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseType":"1","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","uri":"flowmemory://base-canary/rootfield"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","observationId":"0xdd701330fee35840b3e02fc8b8154284d883008467936052963eb8ed0b5e9b68","occurredAt":"1778700361","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"1","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","uri":"flowmemory://uniswap-v4/after-swap"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0xe331e74d3287b677c4eee492f00c22efd12f9365070eca9569f78966e499e039","observationId":"0x3711d0558240758a09429d6408eb1e8d699bf2555fb8253ef8192fe5f583f12d","occurredAt":"1778700413","parentPulseId":"0xa62ffb4b36a415032949138edbdcba5005de2e35952df88bdf592d4266184b87","pulseId":"0x72407268a2ea62659d6b0f62800931936cc6ea7ea5f5b6db91801ba2f8b43eab","pulseType":"2","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x4a7b8601c06c20bcc7b69c05c51980c12dbd50cbd95a59f460d40555bfc37ce3","uri":"flowmemory://base-canary/root"},{"actor":"0x3a6fba5a78216ba3a8da8d8f501dee2c8186aff9","commitment":"0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9","observationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","occurredAt":"1778700417","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0x16c2adf5f3e46ee91d16a432d2420c566851b311e767860cab99068dcaca2591","pulseType":"4","rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114","sequence":"2","subject":"0x205b396f8fdc1b25ddef7afa02f31f51f1e1048004897b87477569a8e573d434","uri":"flowmemory://uniswap-v4/after-swap"}],"rejectedLogs":[],"rootfields":[{"firstObservationId":"0x7b43ea15b0d77169e56395425bb757d280aa8be2b262fbd5f85e68fb01cb33b9","latestObservationId":"0xf3a1c63cf9eb89e59970b1266f8691142349d2af230a7654abcf91fcccef0da6","pulseCount":4,"rootfieldId":"0x19c830e926bfd3ce06d71ed0ef2e90ddc73accf4367b0defea835dc1cd3b3114"}],"schema":"flowmemory.indexer.state.v0","source":"base-mainnet-canary-rpc"}} diff --git a/fixtures/deployments/base-sepolia-rehearsal-plan.json b/fixtures/deployments/base-sepolia-rehearsal-plan.json new file mode 100644 index 00000000..0b2b0d41 --- /dev/null +++ b/fixtures/deployments/base-sepolia-rehearsal-plan.json @@ -0,0 +1,95 @@ +{ + "schema": "flowmemory.base_sepolia.deployment_rehearsal_plan.v0", + "generatedAt": "2026-05-13T23:56:47.606Z", + "mode": "plan-only", + "productionReady": false, + "network": { + "name": "Base Sepolia", + "chainId": "84532", + "explorer": "https://sepolia.basescan.org" + }, + "environment": { + "BASE_SEPOLIA_RPC_URL": "", + "BASE_SEPOLIA_DEPLOYER_KEY_HEX": "", + "BASE_SEPOLIA_BASESCAN_API_KEY": "", + "BASESCAN_API_KEY": "", + "BASE_SEPOLIA_FLOWPULSE_ADDRESSES": "", + "BASE_SEPOLIA_FROM_BLOCK": "", + "BASE_SEPOLIA_TO_BLOCK": "", + "BASE_SEPOLIA_FINALIZED_BLOCK": "", + "BASE_SEPOLIA_REHEARSAL_PLAN_OUT": "", + "BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT": "" + }, + "requiredEnv": [ + "BASE_SEPOLIA_RPC_URL", + "BASE_SEPOLIA_DEPLOYER_KEY_HEX" + ], + "optionalEnv": [ + "BASE_SEPOLIA_BASESCAN_API_KEY", + "BASESCAN_API_KEY", + "BASE_SEPOLIA_FLOWPULSE_ADDRESSES", + "BASE_SEPOLIA_FROM_BLOCK", + "BASE_SEPOLIA_TO_BLOCK", + "BASE_SEPOLIA_FINALIZED_BLOCK", + "BASE_SEPOLIA_REHEARSAL_PLAN_OUT", + "BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT", + "BASE_SEPOLIA_REHEARSAL_GENERATED_AT" + ], + "deploys": [ + "RootfieldRegistry", + "FlowMemoryHookAdapter", + "ArtifactRegistry", + "CursorRegistry", + "ReceiptVerifier", + "WorkerRegistry", + "VerifierRegistry", + "WorkReceiptRegistry", + "VerifierReportRegistry", + "WorkDebtScheduler" + ], + "forgeCommand": { + "program": "forge", + "args": [ + "script", + "script/DeployLaunchCandidate.s.sol:DeployLaunchCandidate", + "--rpc-url", + "$BASE_SEPOLIA_RPC_URL", + "--private-key", + "$BASE_SEPOLIA_DEPLOYER_KEY_HEX", + "--chain-id", + "84532" + ], + "broadcast": false, + "note": "Dry run is default. Add --broadcast only when the operator has funded the deployer and accepts testnet gas spend." + }, + "writeRehearsal": { + "rootfieldRegistration": [ + "cast send \"registerRootfield(bytes32,bytes32,bytes32,string)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX", + "cast send \"submitRoot(bytes32,bytes32,bytes32,bytes32,string)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX" + ], + "swapMemorySignalAdapter": [ + "cast send \"afterSwap(address,bytes32,bytes32,bytes32,bytes)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX" + ] + }, + "readRehearsal": { + "rootfieldReadback": "cast call \"getRootfield(bytes32)\" --rpc-url $BASE_SEPOLIA_RPC_URL", + "flowPulseReader": "npm run index:base-sepolia -- --rpc-url $BASE_SEPOLIA_RPC_URL --address --address --from-block --to-block --finalized-block ", + "resumeReader": "npm run index:base-sepolia -- --rpc-url $BASE_SEPOLIA_RPC_URL --address --address --resume-from-checkpoint --to-block " + }, + "verification": { + "apiKeyEnv": "BASE_SEPOLIA_BASESCAN_API_KEY or BASESCAN_API_KEY", + "command": "forge verify-contract --chain-id 84532
--etherscan-api-key $BASE_SEPOLIA_BASESCAN_API_KEY" + }, + "rollback": [ + "Do not reuse a partially failed deployment artifact as canonical.", + "If a contract deployment fails before broadcast completion, discard the artifact and rerun dry-run before broadcast.", + "If smoke writes fail after contracts deploy, record the failed transaction hash, pause claims, and redeploy only if contract ownership or constructor inputs are wrong.", + "No upgrade or proxy rollback exists in V0; rollback means stop using the address set and mark it superseded in docs/DEPLOYMENTS." + ], + "boundaries": [ + "Base Sepolia is a public testnet rehearsal, not production mainnet readiness.", + "The V0 hook adapter is not a production Uniswap v4 PoolManager hook.", + "txHash, transactionIndex, and logIndex are derived by the reader after receipts exist, never inside the hook.", + "No private keys, RPC credentials, or explorer API keys are written to artifacts." + ] +} diff --git a/infra/scripts/bridge-base-sepolia-smoke.ps1 b/infra/scripts/bridge-base-sepolia-smoke.ps1 index f25f1dd0..c55cd097 100644 --- a/infra/scripts/bridge-base-sepolia-smoke.ps1 +++ b/infra/scripts/bridge-base-sepolia-smoke.ps1 @@ -1,25 +1,49 @@ param( - [Parameter(Mandatory = $true)] - [string]$RpcUrl, + [string]$RpcUrl = $env:BASE_SEPOLIA_RPC_URL, - [Parameter(Mandatory = $true)] - [string]$LockboxAddress, + [string]$LockboxAddress = $env:BASE_SEPOLIA_BRIDGE_LOCKBOX_ADDRESS, - [Parameter(Mandatory = $true)] - [string]$FromBlock, + [string]$FromBlock = $env:BASE_SEPOLIA_BRIDGE_FROM_BLOCK, - [Parameter(Mandatory = $true)] - [string]$ToBlock, + [string]$ToBlock = $env:BASE_SEPOLIA_BRIDGE_TO_BLOCK, - [string]$Out = "services/bridge-relayer/out/base-sepolia-bridge-observation.json", + [string]$Out = $(if ($env:BASE_SEPOLIA_BRIDGE_OBSERVATION_OUT) { $env:BASE_SEPOLIA_BRIDGE_OBSERVATION_OUT } else { "services/bridge-relayer/out/base-sepolia-bridge-observation.json" }), - [string]$CreditOut = "services/bridge-relayer/out/base-sepolia-bridge-credit.json", + [string]$CreditOut = $(if ($env:BASE_SEPOLIA_BRIDGE_CREDIT_OUT) { $env:BASE_SEPOLIA_BRIDGE_CREDIT_OUT } else { "services/bridge-relayer/out/base-sepolia-bridge-credit.json" }), - [string]$HandoffOut = "services/bridge-relayer/out/base-sepolia-bridge-handoff.json" + [string]$HandoffOut = $(if ($env:BASE_SEPOLIA_BRIDGE_HANDOFF_OUT) { $env:BASE_SEPOLIA_BRIDGE_HANDOFF_OUT } else { "services/bridge-relayer/out/base-sepolia-bridge-handoff.json" }) ) $ErrorActionPreference = "Stop" +function Require-Value { + param( + [string]$Name, + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "$Name is required. Pass the parameter explicitly or set the matching Base Sepolia environment variable." + } +} + +Require-Value -Name "BASE_SEPOLIA_RPC_URL / -RpcUrl" -Value $RpcUrl +Require-Value -Name "BASE_SEPOLIA_BRIDGE_LOCKBOX_ADDRESS / -LockboxAddress" -Value $LockboxAddress +Require-Value -Name "BASE_SEPOLIA_BRIDGE_FROM_BLOCK / -FromBlock" -Value $FromBlock +Require-Value -Name "BASE_SEPOLIA_BRIDGE_TO_BLOCK / -ToBlock" -Value $ToBlock + +if ($LockboxAddress -notmatch '^0x[0-9a-fA-F]{40}$') { + throw "LockboxAddress must be a 20-byte hex address." +} + +if ($FromBlock -notmatch '^\d+$' -or $ToBlock -notmatch '^\d+$') { + throw "FromBlock and ToBlock must be decimal block numbers." +} + +if ([UInt64]$FromBlock -gt [UInt64]$ToBlock) { + throw "FromBlock must be less than or equal to ToBlock." +} + $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) Set-Location -LiteralPath $repoRoot diff --git a/infra/scripts/check-unsafe-claims.mjs b/infra/scripts/check-unsafe-claims.mjs index 74f28d86..26707e02 100755 --- a/infra/scripts/check-unsafe-claims.mjs +++ b/infra/scripts/check-unsafe-claims.mjs @@ -3,12 +3,21 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; const root = process.cwd(); -const scanRoots = ["README.md", "docs", "marketing"].filter((entry) => existsSync(join(root, entry))); +const scanRoots = ["README.md", "docs", "contracts", "marketing"].filter((entry) => existsSync(join(root, entry))); const forbiddenClaims = [ { name: "production-ready", pattern: /\bproduction[- ]ready\b/i }, { name: "mainnet-ready", pattern: /\bmainnet[- ]ready\b/i }, + { name: "production launch", pattern: /\bproduction\s+launch\b/i }, + { name: "mainnet launch", pattern: /\bmainnet\s+launch\b/i }, + { name: "production mainnet", pattern: /\bproduction[- ]mainnet\b/i }, + { name: "production deployment", pattern: /\bproduction\s+deployment\b/i }, { name: "production L1", pattern: /\bproduction\s+L1\b/i }, + { name: "production verifier network", pattern: /\bproduction\s+verifier\s+network\b/i }, + { name: "production Uniswap hook", pattern: /\bproduction\s+Uniswap\s+v4\s+hook\b|\bproduction\s+hook\s+deployment\b/i }, + { name: "production bridge", pattern: /\bproduction\s+bridge\b/i }, + { name: "production custody", pattern: /\bproduction\s+(wallet\s+)?custody\b/i }, + { name: "audited", pattern: /\baudited\b/i }, { name: "free storage", pattern: /\bfree\s+storage\b|\bstorage\s+is\s+free\b/i }, { name: "AI on-chain", pattern: /\bAI\s+(runs|running)\s+on[- ]chain\b|\bon[- ]chain\s+AI\b/i }, { name: "fully trustless", pattern: /\bfully\s+trustless\b|\bfull\s+trustless\b/i }, @@ -17,9 +26,9 @@ const forbiddenClaims = [ ]; const allowedLineContext = - /\b(not|no|never|cannot|can't|do not|does not|without|blocked|forbid|forbidden|avoid|out of scope|non-goal|non-goals|boundary|boundaries|guardrail|guardrails|unsafe claim|not allowed|later gated|blocked until|must not|remain blocked)\b/i; + /\b(not|no|never|cannot|can't|do not|does not|without|before|blocked|forbid|forbidden|reject|rejected|avoid|out of scope|non-goal|non-goals|boundary|boundaries|guardrail|guardrails|unsafe claim|not allowed|gated|later gated|blocked until|must not|remain blocked)\b/i; const allowedHeadingContext = - /\b(not|non-goal|non-goals|blocked|guardrail|guardrails|boundary|boundaries|out of scope|conceptual|not implemented|later gated|do not|unsafe|what not to claim|avoid)\b/i; + /\b(not|non-goal|non-goals|blocked|reject|rejected|guardrail|guardrails|boundary|boundaries|out of scope|conceptual|not implemented|gated|later gated|do not|unsafe|what not to claim|avoid)\b/i; const startsGuardedList = /\b(not allowed|claims that remain blocked|current launch target is not|reject or send back|stop and ask|what not to claim|avoid)\b/i; @@ -53,6 +62,7 @@ for (const file of scanRoots.flatMap(listFiles)) { let guardedListLinesRemaining = 0; lines.forEach((line, index) => { + const lineContext = [lines[index - 1] ?? "", line, lines[index + 1] ?? ""].join(" "); if (/^#{1,6}\s+/.test(line)) { headingAllowsForbiddenClaims = allowedHeadingContext.test(line); } @@ -64,7 +74,7 @@ for (const file of scanRoots.flatMap(listFiles)) { if (!claim.pattern.test(line)) { continue; } - if (allowedLineContext.test(line) || headingAllowsForbiddenClaims || guardedListLinesRemaining > 0) { + if (allowedLineContext.test(lineContext) || headingAllowsForbiddenClaims || guardedListLinesRemaining > 0) { continue; } violations.push(`${rel}:${index + 1}: ${claim.name}: ${line.trim()}`); diff --git a/infra/scripts/run-base-sepolia-deploy.mjs b/infra/scripts/run-base-sepolia-deploy.mjs index 05374359..6c0ed11e 100644 --- a/infra/scripts/run-base-sepolia-deploy.mjs +++ b/infra/scripts/run-base-sepolia-deploy.mjs @@ -1,20 +1,201 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; -const args = new Set(process.argv.slice(2)); +const rawArgs = process.argv.slice(2); +const args = new Set(rawArgs); const broadcast = args.has("--broadcast"); +const planOnly = args.has("--plan-only"); +const json = args.has("--json"); + +function readArgValue(name, fallback) { + const index = rawArgs.indexOf(name); + if (index === -1) return fallback; + const value = rawArgs[index + 1]; + if (value === undefined || value.startsWith("--")) { + throw new Error(`${name} requires a value`); + } + return value; +} + +const planOut = resolve( + readArgValue( + "--plan-out", + process.env.BASE_SEPOLIA_REHEARSAL_PLAN_OUT ?? "fixtures/deployments/base-sepolia-rehearsal-plan.json", + ), +); +const artifactOut = resolve( + readArgValue( + "--artifact-out", + process.env.BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT ?? "fixtures/deployments/base-sepolia-rehearsal.latest.json", + ), +); const rpcUrl = process.env.BASE_SEPOLIA_RPC_URL; const deployerKey = process.env.BASE_SEPOLIA_DEPLOYER_KEY_HEX; +const basescanApiKey = process.env.BASE_SEPOLIA_BASESCAN_API_KEY ?? process.env.BASESCAN_API_KEY; +const explicitGeneratedAt = readArgValue( + "--generated-at", + process.env.BASE_SEPOLIA_REHEARSAL_GENERATED_AT ?? null, +); + +function readExistingGeneratedAt(path) { + if (!existsSync(path)) return null; + try { + const parsed = JSON.parse(readFileSync(path, "utf8")); + return typeof parsed.generatedAt === "string" ? parsed.generatedAt : null; + } catch { + return null; + } +} -if (!rpcUrl) { - throw new Error("BASE_SEPOLIA_RPC_URL is required for Base Sepolia deployment runs"); +const generatedAt = explicitGeneratedAt ?? (planOnly ? readExistingGeneratedAt(planOut) : null) ?? new Date().toISOString(); + +function writeJson(path, value) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function validateEnvForForgeRun() { + if (!rpcUrl) { + throw new Error("BASE_SEPOLIA_RPC_URL is required for Base Sepolia deployment runs"); + } + if (!deployerKey) { + throw new Error("BASE_SEPOLIA_DEPLOYER_KEY_HEX is required for Base Sepolia deployment runs"); + } + if (!/^0x[0-9a-fA-F]{64}$/.test(deployerKey)) { + throw new Error("BASE_SEPOLIA_DEPLOYER_KEY_HEX must be a 32-byte hex private key with 0x prefix"); + } } -if (!deployerKey) { - throw new Error("BASE_SEPOLIA_DEPLOYER_KEY_HEX is required for Base Sepolia deployment runs"); +function redactedEnv() { + return { + BASE_SEPOLIA_RPC_URL: rpcUrl ? "" : "", + BASE_SEPOLIA_DEPLOYER_KEY_HEX: deployerKey ? "" : "", + BASE_SEPOLIA_BASESCAN_API_KEY: process.env.BASE_SEPOLIA_BASESCAN_API_KEY ? "" : "", + BASESCAN_API_KEY: process.env.BASESCAN_API_KEY ? "" : "", + BASE_SEPOLIA_FLOWPULSE_ADDRESSES: process.env.BASE_SEPOLIA_FLOWPULSE_ADDRESSES ? "" : "", + BASE_SEPOLIA_FROM_BLOCK: process.env.BASE_SEPOLIA_FROM_BLOCK ? "" : "", + BASE_SEPOLIA_TO_BLOCK: process.env.BASE_SEPOLIA_TO_BLOCK ? "" : "", + BASE_SEPOLIA_FINALIZED_BLOCK: process.env.BASE_SEPOLIA_FINALIZED_BLOCK ? "" : "", + BASE_SEPOLIA_REHEARSAL_PLAN_OUT: process.env.BASE_SEPOLIA_REHEARSAL_PLAN_OUT ? "" : "", + BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT: process.env.BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT + ? "" + : "", + }; } +function deploymentPlan() { + const forgeArgsRedacted = [ + "script", + "script/DeployLaunchCandidate.s.sol:DeployLaunchCandidate", + "--rpc-url", + "$BASE_SEPOLIA_RPC_URL", + "--private-key", + "$BASE_SEPOLIA_DEPLOYER_KEY_HEX", + "--chain-id", + "84532", + ]; + if (broadcast) { + forgeArgsRedacted.push("--broadcast", "--slow"); + } + + return { + schema: "flowmemory.base_sepolia.deployment_rehearsal_plan.v0", + generatedAt, + mode: planOnly ? "plan-only" : broadcast ? "broadcast" : "dry-run", + productionReady: false, + network: { + name: "Base Sepolia", + chainId: "84532", + explorer: "https://sepolia.basescan.org", + }, + environment: redactedEnv(), + requiredEnv: [ + "BASE_SEPOLIA_RPC_URL", + "BASE_SEPOLIA_DEPLOYER_KEY_HEX", + ], + optionalEnv: [ + "BASE_SEPOLIA_BASESCAN_API_KEY", + "BASESCAN_API_KEY", + "BASE_SEPOLIA_FLOWPULSE_ADDRESSES", + "BASE_SEPOLIA_FROM_BLOCK", + "BASE_SEPOLIA_TO_BLOCK", + "BASE_SEPOLIA_FINALIZED_BLOCK", + "BASE_SEPOLIA_REHEARSAL_PLAN_OUT", + "BASE_SEPOLIA_REHEARSAL_ARTIFACT_OUT", + "BASE_SEPOLIA_REHEARSAL_GENERATED_AT", + ], + deploys: [ + "RootfieldRegistry", + "FlowMemoryHookAdapter", + "ArtifactRegistry", + "CursorRegistry", + "ReceiptVerifier", + "WorkerRegistry", + "VerifierRegistry", + "WorkReceiptRegistry", + "VerifierReportRegistry", + "WorkDebtScheduler", + ], + forgeCommand: { + program: "forge", + args: forgeArgsRedacted, + broadcast, + note: "Dry run is default. Add --broadcast only when the operator has funded the deployer and accepts testnet gas spend.", + }, + writeRehearsal: { + rootfieldRegistration: [ + "cast send \"registerRootfield(bytes32,bytes32,bytes32,string)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX", + "cast send \"submitRoot(bytes32,bytes32,bytes32,bytes32,string)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX", + ], + swapMemorySignalAdapter: [ + "cast send \"afterSwap(address,bytes32,bytes32,bytes32,bytes)\" --rpc-url $BASE_SEPOLIA_RPC_URL --private-key $BASE_SEPOLIA_DEPLOYER_KEY_HEX", + ], + }, + readRehearsal: { + rootfieldReadback: + "cast call \"getRootfield(bytes32)\" --rpc-url $BASE_SEPOLIA_RPC_URL", + flowPulseReader: + "npm run index:base-sepolia -- --rpc-url $BASE_SEPOLIA_RPC_URL --address --address --from-block --to-block --finalized-block ", + resumeReader: + "npm run index:base-sepolia -- --rpc-url $BASE_SEPOLIA_RPC_URL --address --address --resume-from-checkpoint --to-block ", + }, + verification: { + apiKeyEnv: basescanApiKey ? "" : "BASE_SEPOLIA_BASESCAN_API_KEY or BASESCAN_API_KEY", + command: + "forge verify-contract --chain-id 84532
--etherscan-api-key $BASE_SEPOLIA_BASESCAN_API_KEY", + }, + rollback: [ + "Do not reuse a partially failed deployment artifact as canonical.", + "If a contract deployment fails before broadcast completion, discard the artifact and rerun dry-run before broadcast.", + "If smoke writes fail after contracts deploy, record the failed transaction hash, pause claims, and redeploy only if contract ownership or constructor inputs are wrong.", + "No upgrade or proxy rollback exists in V0; rollback means stop using the address set and mark it superseded in docs/DEPLOYMENTS.", + ], + boundaries: [ + "Base Sepolia is a public testnet rehearsal, not production mainnet readiness.", + "The V0 hook adapter is not a production Uniswap v4 PoolManager hook.", + "txHash, transactionIndex, and logIndex are derived by the reader after receipts exist, never inside the hook.", + "No private keys, RPC credentials, or explorer API keys are written to artifacts.", + ], + }; +} + +const plan = deploymentPlan(); +writeJson(planOut, plan); + +if (planOnly) { + if (json) { + console.log(JSON.stringify({ ok: true, planOut, plan }, null, 2)); + } else { + console.log(`Base Sepolia deployment rehearsal plan written: ${planOut}`); + } + process.exit(0); +} + +validateEnvForForgeRun(); + const forgeArgs = [ "script", "script/DeployLaunchCandidate.s.sol:DeployLaunchCandidate", @@ -22,6 +203,8 @@ const forgeArgs = [ rpcUrl, "--private-key", deployerKey, + "--chain-id", + "84532", ]; if (broadcast) { @@ -37,3 +220,33 @@ const result = spawnSync("forge", forgeArgs, { if (result.status !== 0) { throw new Error(`Base Sepolia deploy ${broadcast ? "broadcast" : "dry run"} failed with exit code ${result.status ?? "unknown"}`); } + +const artifact = { + schema: "flowmemory.base_sepolia.deployment_rehearsal_artifact.v0", + generatedAt, + mode: broadcast ? "broadcast" : "dry-run", + productionReady: false, + network: plan.network, + planPath: planOut, + expectedFoundryBroadcastDir: "broadcast/DeployLaunchCandidate.s.sol/84532", + environment: redactedEnv(), + forge: { + status: result.status, + command: plan.forgeCommand, + }, + nextSteps: { + readback: plan.readRehearsal, + writeSmoke: plan.writeRehearsal, + sourceVerification: plan.verification, + rollback: plan.rollback, + }, + boundaries: plan.boundaries, +}; + +writeJson(artifactOut, artifact); + +if (json) { + console.log(JSON.stringify({ ok: true, artifactOut, artifact }, null, 2)); +} else { + console.log(`Base Sepolia ${broadcast ? "broadcast" : "dry run"} artifact written: ${artifactOut}`); +} diff --git a/package.json b/package.json index b0ae96e7..24b8ecf0 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "test": "npm test --prefix services/shared && npm test --prefix services/indexer && npm test --prefix services/verifier && npm test --prefix services/flowmemory && npm test --prefix services/control-plane && npm test --prefix services/bridge-relayer", "contracts:hardening": "node infra/scripts/run-contract-hardening.mjs", "contracts:hardening:slither": "node infra/scripts/run-contract-hardening.mjs --require-slither", - "index:base-canary": "npm run index:base-canary --prefix services/indexer", - "read:base-canary": "npm run index:base-canary --prefix services/indexer", - "index:base-sepolia": "npm run index:base-sepolia --prefix services/indexer", - "read:base-sepolia": "npm run index:base-sepolia --prefix services/indexer", + "index:base-canary": "npm --prefix services/indexer run index:base-canary --", + "read:base-canary": "npm --prefix services/indexer run index:base-canary --", + "index:base-sepolia": "npm --prefix services/indexer run index:base-sepolia --", + "read:base-sepolia": "npm --prefix services/indexer run index:base-sepolia --", "deploy:base-sepolia": "node infra/scripts/run-base-sepolia-deploy.mjs", + "deploy:base-sepolia:plan": "node infra/scripts/run-base-sepolia-deploy.mjs --plan-only", "deploy:base-sepolia:broadcast": "node infra/scripts/run-base-sepolia-deploy.mjs --broadcast", "verify:base-canary:sources": "node infra/scripts/verify-base-canary-sources.mjs --dry-run", "verify:base-canary:sources:submit": "node infra/scripts/verify-base-canary-sources.mjs --submit --watch", diff --git a/script/DeployLaunchCandidate.s.sol b/script/DeployLaunchCandidate.s.sol index ac53d41d..59c64b53 100644 --- a/script/DeployLaunchCandidate.s.sol +++ b/script/DeployLaunchCandidate.s.sol @@ -23,6 +23,9 @@ interface Vm { /// protocol, proxy upgrade path, token, custody system, or verifier network. contract DeployLaunchCandidate { Vm private constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + uint256 internal constant BASE_SEPOLIA_CHAIN_ID = 84532; + + error UnexpectedChain(uint256 expected, uint256 actual); struct Deployment { address rootfieldRegistry; @@ -51,6 +54,10 @@ contract DeployLaunchCandidate { ); function run() external returns (Deployment memory deployment) { + if (block.chainid != BASE_SEPOLIA_CHAIN_ID) { + revert UnexpectedChain(BASE_SEPOLIA_CHAIN_ID, block.chainid); + } + VM.startBroadcast(); deployment = Deployment({ diff --git a/services/flowmemory/src/generate-canary-dashboard.ts b/services/flowmemory/src/generate-canary-dashboard.ts index 17da84b0..ffb167dd 100644 --- a/services/flowmemory/src/generate-canary-dashboard.ts +++ b/services/flowmemory/src/generate-canary-dashboard.ts @@ -78,6 +78,9 @@ interface CanaryCheckpoint { rejectedLogCount: number; duplicateCount: number; lastIndexedBlock: string; + highestObservedBlock: string | null; + nextFromBlock: string; + emptyRange: boolean; generatedAt: string; safety: { productionReady: false; diff --git a/services/indexer/README.md b/services/indexer/README.md index 1c20ff07..69def784 100644 --- a/services/indexer/README.md +++ b/services/indexer/README.md @@ -9,6 +9,7 @@ From the repository root: ```powershell npm run index:fixtures npm run index:base-sepolia -- --rpc-url --address --from-block --to-block +npm run index:base-sepolia -- --rpc-url --address --resume-from-checkpoint --to-block npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url --address --from-block --to-block npm run demo:indexer npm test --prefix services/indexer @@ -39,6 +40,16 @@ Use custom output paths: npm run index:base-sepolia -- --rpc-url --address --from-block 123456 --to-block 123999 --finalized-block 123900 --out out/base-sepolia-state.json --checkpoint-out out/base-sepolia-checkpoint.json ``` +Resume from the previous checkpoint: + +```powershell +npm run index:base-sepolia -- --rpc-url --address --resume-from-checkpoint --to-block 124999 --checkpoint-out out/base-sepolia-checkpoint.json +``` + +The Base Sepolia reader refuses broad scans above the configured block span +(`10,000` blocks by default). Use `--max-block-span ` only for explicit +operator-approved testnet reads. + ## Fixtures Primary receipt fixtures: @@ -168,7 +179,9 @@ The state itself declares: flowmemory.indexer.state.v0 ``` -JSON output is deterministic and contains observations, cursors, batches, rootfields, pulses, rejected logs, and duplicate records. +JSON output is deterministic and contains observations, cursors, batches, +rootfields, pulses, rejected logs, duplicate records, and a dashboard feed +summary for status/warning display. The Base Sepolia reader also writes a durable checkpoint: @@ -176,7 +189,11 @@ The Base Sepolia reader also writes a durable checkpoint: flowmemory.indexer.base_sepolia_checkpoint.v0 ``` -The checkpoint records the network, chain id, emitting addresses, scan range, finality threshold, state path, counts, and latest indexed block. It intentionally does not store RPC URLs or private keys. +The checkpoint records the network, chain id, emitting addresses, scan range, +finality threshold, state path, counts, `lastIndexedBlock`, +`highestObservedBlock`, `nextFromBlock`, and `emptyRange`. It intentionally does +not store RPC URLs or private keys. State and checkpoint writes use a temporary +file plus rename so a partial write does not replace the last good JSON file. The Base mainnet canary reader writes: diff --git a/services/indexer/fixtures/indexer-state.schema.json b/services/indexer/fixtures/indexer-state.schema.json index 2ffe3c0b..056fac12 100644 --- a/services/indexer/fixtures/indexer-state.schema.json +++ b/services/indexer/fixtures/indexer-state.schema.json @@ -16,18 +16,53 @@ "cursors", "batches", "rejectedLogs", - "duplicates" + "duplicates", + "dashboardFeed" ], "properties": { "schema": { "const": "flowmemory.indexer.state.v0" }, - "source": { "enum": ["fixture", "local-rpc-placeholder", "base-sepolia-rpc"] }, + "source": { "enum": ["fixture", "local-rpc-placeholder", "base-sepolia-rpc", "base-mainnet-canary-rpc"] }, "observations": { "type": "array" }, "pulses": { "type": "array" }, "rootfields": { "type": "array" }, "cursors": { "type": "array" }, "batches": { "type": "array" }, "rejectedLogs": { "type": "array" }, - "duplicates": { "type": "array" } + "duplicates": { "type": "array" }, + "dashboardFeed": { + "type": "object", + "required": [ + "schema", + "source", + "chainId", + "sourceSetId", + "observationCount", + "dashboardCanonicalObservationCount", + "rejectedLogCount", + "duplicateCount", + "duplicateKindCounts", + "rejectedReasonCounts", + "warningCodes", + "hasIntegrityWarnings", + "observations" + ], + "properties": { + "schema": { "const": "flowmemory.indexer.dashboard_feed.v0" }, + "source": { "type": "string" }, + "chainId": { "type": "string" }, + "sourceSetId": { "type": "string" }, + "observationCount": { "type": "number" }, + "dashboardCanonicalObservationCount": { "type": "number" }, + "rejectedLogCount": { "type": "number" }, + "duplicateCount": { "type": "number" }, + "duplicateKindCounts": { "type": "object" }, + "rejectedReasonCounts": { "type": "object" }, + "warningCodes": { "type": "array" }, + "hasIntegrityWarnings": { "type": "boolean" }, + "observations": { "type": "array" } + }, + "additionalProperties": false + } }, "additionalProperties": false } diff --git a/services/indexer/out/indexer-state.json b/services/indexer/out/indexer-state.json index de430ea3..15792975 100644 --- a/services/indexer/out/indexer-state.json +++ b/services/indexer/out/indexer-state.json @@ -1 +1 @@ -{"schema":"flowmemory.indexer.persistence.v0","state":{"batches":[{"cursorCount":7,"observationCount":8,"rejectedLogCount":2,"schema":"flowmemory.indexer.batch.v0","source":"fixture","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88"}],"cursors":[{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","chainId":"8453","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","logIndex":"2","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"7"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222226","blockNumber":"123460","chainId":"8453","cursorId":"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec","logIndex":"6","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"11"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222223","blockNumber":"123457","chainId":"8453","cursorId":"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b","logIndex":"3","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"8"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222227","blockNumber":"123461","chainId":"8453","cursorId":"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a","logIndex":"7","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"12"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222225","blockNumber":"123459","chainId":"8453","cursorId":"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40","logIndex":"5","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"10"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222224","blockNumber":"123458","chainId":"8453","cursorId":"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7","logIndex":"4","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"9"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222230","blockNumber":"123457","chainId":"8453","cursorId":"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959","logIndex":"10","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"15"}],"duplicates":[{"kind":"exactDuplicate","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],"observations":[{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"blockNumber\":\"123456\",\"chainId\":\"8453\",\"commitment\":\"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5\",\"cursorId\":\"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"2\",\"observationId\":\"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91\",\"occurredAt\":\"1778640000\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"1\",\"subject\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"transactionIndex\":\"7\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333333\",\"uri\":\"ipfs://bafy-flowmemory-example\"}","chainId":"8453","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222223","blockNumber":"123457","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222223\",\"blockNumber\":\"123457\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"3\",\"observationId\":\"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea\",\"occurredAt\":\"1778640060\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"2\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"8\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333334\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"3","observationId":"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"8","txHash":"0x3333333333333333333333333333333333333333333333333333333333333334","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222230","blockNumber":"123457","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222230\",\"blockNumber\":\"123457\",\"chainId\":\"8453\",\"commitment\":\"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795\",\"cursorId\":\"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959\",\"emittingContract\":\"0x2222222222222222222222222222222222222222\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"10\",\"observationId\":\"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e\",\"occurredAt\":\"1778648420\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"6\",\"subject\":\"0x1212121212121212121212121212121212121212121212121212121212121212\",\"transactionIndex\":\"15\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333341\",\"uri\":\"fixture://swap-memory-valid\"}","chainId":"8453","commitment":"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795","cursorId":"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959","duplicateKind":"unique","emittingContract":"0x2222222222222222222222222222222222222222","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"10","observationId":"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e","occurredAt":"1778648420","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008","pulseType":"4","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"6","subject":"0x1212121212121212121212121212121212121212121212121212121212121212","transactionIndex":"15","txHash":"0x3333333333333333333333333333333333333333333333333333333333333341","uri":"fixture://swap-memory-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"blockNumber\":\"123456\",\"chainId\":\"8453\",\"commitment\":\"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5\",\"cursorId\":\"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"2\",\"observationId\":\"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91\",\"occurredAt\":\"1778640000\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"1\",\"subject\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"transactionIndex\":\"7\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333333\",\"uri\":\"ipfs://bafy-flowmemory-example\"}","chainId":"8453","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","duplicateKind":"exactDuplicate","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222224","blockNumber":"123458","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222224\",\"blockNumber\":\"123458\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"removed\",\"logIndex\":\"4\",\"observationId\":\"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79\",\"occurredAt\":\"1778640060\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"2\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"9\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333335\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"removed","logIndex":"4","observationId":"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"9","txHash":"0x3333333333333333333333333333333333333333333333333333333333333335","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222225","blockNumber":"123459","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222225\",\"blockNumber\":\"123459\",\"chainId\":\"8453\",\"commitment\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"cursorId\":\"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"5\",\"observationId\":\"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df\",\"occurredAt\":\"1778640120\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"3\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"10\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333336\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","cursorId":"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"5","observationId":"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df","occurredAt":"1778640120","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"3","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"10","txHash":"0x3333333333333333333333333333333333333333333333333333333333333336","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222226","blockNumber":"123460","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222226\",\"blockNumber\":\"123460\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"6\",\"observationId\":\"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4\",\"occurredAt\":\"1778640180\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"4\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"11\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333337\",\"uri\":\"fixture://missing-artifact\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"6","observationId":"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4","occurredAt":"1778640180","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"4","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"11","txHash":"0x3333333333333333333333333333333333333333333333333333333333333337","uri":"fixture://missing-artifact"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222227","blockNumber":"123461","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222227\",\"blockNumber\":\"123461\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"7\",\"observationId\":\"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98\",\"occurredAt\":\"1778640240\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005\",\"pulseType\":\"99\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"5\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"12\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333338\",\"uri\":\"fixture://unsupported\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"7","observationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","occurredAt":"1778640240","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005","pulseType":"99","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"5","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"12","txHash":"0x3333333333333333333333333333333333333333333333333333333333333338","uri":"fixture://unsupported"}],"pulses":[{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795","observationId":"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e","occurredAt":"1778648420","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008","pulseType":"4","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"6","subject":"0x1212121212121212121212121212121212121212121212121212121212121212","uri":"fixture://swap-memory-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","observationId":"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df","occurredAt":"1778640120","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"3","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4","occurredAt":"1778640180","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"4","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://missing-artifact"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","occurredAt":"1778640240","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005","pulseType":"99","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"5","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://unsupported"}],"rejectedLogs":[{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222228","blockNumber":"123462","chainId":"8453","logIndex":"8","message":"receipt status is reverted","reasonCode":"receipt.reverted","transactionIndex":"13","txHash":"0x3333333333333333333333333333333333333333333333333333333333333339"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222229","blockNumber":"123463","chainId":"8453","logIndex":"9","message":"unsupported event signature: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","reasonCode":"log.malformed","transactionIndex":"14","txHash":"0x3333333333333333333333333333333333333333333333333333333333333340"}],"rootfields":[{"firstObservationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","latestObservationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","pulseCount":7,"rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}],"schema":"flowmemory.indexer.state.v0","source":"fixture"}} +{"schema":"flowmemory.indexer.persistence.v0","state":{"batches":[{"cursorCount":7,"observationCount":8,"rejectedLogCount":2,"schema":"flowmemory.indexer.batch.v0","source":"fixture","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88"}],"cursors":[{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","chainId":"8453","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","logIndex":"2","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"7"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222226","blockNumber":"123460","chainId":"8453","cursorId":"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec","logIndex":"6","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"11"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222223","blockNumber":"123457","chainId":"8453","cursorId":"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b","logIndex":"3","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"8"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222227","blockNumber":"123461","chainId":"8453","cursorId":"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a","logIndex":"7","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"12"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222225","blockNumber":"123459","chainId":"8453","cursorId":"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40","logIndex":"5","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"10"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222224","blockNumber":"123458","chainId":"8453","cursorId":"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7","logIndex":"4","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"9"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222230","blockNumber":"123457","chainId":"8453","cursorId":"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959","logIndex":"10","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","transactionIndex":"15"}],"dashboardFeed":{"chainId":"8453","dashboardCanonicalObservationCount":6,"duplicateCount":1,"duplicateKindCounts":{"exactDuplicate":1},"hasIntegrityWarnings":true,"observationCount":8,"observations":[{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","chainId":"8453","dashboardCanonical":false,"duplicateKind":"exactDuplicate","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222223","blockNumber":"123457","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"finalized","logIndex":"3","observationId":"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea","occurredAt":"1778640060","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","transactionIndex":"8","txHash":"0x3333333333333333333333333333333333333333333333333333333333333334","uri":"fixture://root-commit-valid"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222230","blockNumber":"123457","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x2222222222222222222222222222222222222222","lifecycleState":"finalized","logIndex":"10","observationId":"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e","occurredAt":"1778648420","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008","pulseType":"4","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"6","transactionIndex":"15","txHash":"0x3333333333333333333333333333333333333333333333333333333333333341","uri":"fixture://swap-memory-valid"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222224","blockNumber":"123458","chainId":"8453","dashboardCanonical":false,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"removed","logIndex":"4","observationId":"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79","occurredAt":"1778640060","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","transactionIndex":"9","txHash":"0x3333333333333333333333333333333333333333333333333333333333333335","uri":"fixture://root-commit-valid"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222225","blockNumber":"123459","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"pending","logIndex":"5","observationId":"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df","occurredAt":"1778640120","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"3","transactionIndex":"10","txHash":"0x3333333333333333333333333333333333333333333333333333333333333336","uri":"fixture://root-commit-valid"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222226","blockNumber":"123460","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"pending","logIndex":"6","observationId":"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4","occurredAt":"1778640180","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"4","transactionIndex":"11","txHash":"0x3333333333333333333333333333333333333333333333333333333333333337","uri":"fixture://missing-artifact"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222227","blockNumber":"123461","chainId":"8453","dashboardCanonical":true,"duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","lifecycleState":"pending","logIndex":"7","observationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","occurredAt":"1778640240","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005","pulseType":"99","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"5","transactionIndex":"12","txHash":"0x3333333333333333333333333333333333333333333333333333333333333338","uri":"fixture://unsupported"}],"rejectedLogCount":2,"rejectedReasonCounts":{"log.malformed":1,"receipt.reverted":1},"schema":"flowmemory.indexer.dashboard_feed.v0","source":"fixture","sourceSetId":"0xafd57bb478d919950dce77a66efd7b7a672a2907ba56c4e54810fa554cab8f88","warningCodes":["duplicate.exactDuplicate","lifecycle.removed","rejected.log.malformed","rejected.receipt.reverted"]},"duplicates":[{"kind":"exactDuplicate","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],"observations":[{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"blockNumber\":\"123456\",\"chainId\":\"8453\",\"commitment\":\"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5\",\"cursorId\":\"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"2\",\"observationId\":\"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91\",\"occurredAt\":\"1778640000\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"1\",\"subject\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"transactionIndex\":\"7\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333333\",\"uri\":\"ipfs://bafy-flowmemory-example\"}","chainId":"8453","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222223","blockNumber":"123457","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222223\",\"blockNumber\":\"123457\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"3\",\"observationId\":\"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea\",\"occurredAt\":\"1778640060\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"2\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"8\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333334\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x55101923cb52836ec838db893181593cc123ee59f787a3db43772e87777a023b","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"3","observationId":"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"8","txHash":"0x3333333333333333333333333333333333333333333333333333333333333334","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222230","blockNumber":"123457","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222230\",\"blockNumber\":\"123457\",\"chainId\":\"8453\",\"commitment\":\"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795\",\"cursorId\":\"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959\",\"emittingContract\":\"0x2222222222222222222222222222222222222222\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"10\",\"observationId\":\"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e\",\"occurredAt\":\"1778648420\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008\",\"pulseType\":\"4\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"6\",\"subject\":\"0x1212121212121212121212121212121212121212121212121212121212121212\",\"transactionIndex\":\"15\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333341\",\"uri\":\"fixture://swap-memory-valid\"}","chainId":"8453","commitment":"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795","cursorId":"0xfcfa01e3a2654fbc1670757fb640c0510a92173bfde46ab8fba809b31a3de959","duplicateKind":"unique","emittingContract":"0x2222222222222222222222222222222222222222","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"10","observationId":"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e","occurredAt":"1778648420","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008","pulseType":"4","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"6","subject":"0x1212121212121212121212121212121212121212121212121212121212121212","transactionIndex":"15","txHash":"0x3333333333333333333333333333333333333333333333333333333333333341","uri":"fixture://swap-memory-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222222","blockNumber":"123456","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222222\",\"blockNumber\":\"123456\",\"chainId\":\"8453\",\"commitment\":\"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5\",\"cursorId\":\"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"finalized\",\"logIndex\":\"2\",\"observationId\":\"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91\",\"occurredAt\":\"1778640000\",\"parentPulseId\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseType\":\"1\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"1\",\"subject\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"transactionIndex\":\"7\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333333\",\"uri\":\"ipfs://bafy-flowmemory-example\"}","chainId":"8453","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","cursorId":"0x40aa916686b08058c379bcf17b75e34dd0aeac21410fbcd87eb2616b3e960584","duplicateKind":"exactDuplicate","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"finalized","logIndex":"2","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","transactionIndex":"7","txHash":"0x3333333333333333333333333333333333333333333333333333333333333333","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222224","blockNumber":"123458","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222224\",\"blockNumber\":\"123458\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"removed\",\"logIndex\":\"4\",\"observationId\":\"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79\",\"occurredAt\":\"1778640060\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"2\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"9\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333335\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0xf494cda844fee1ba70247a5c5853b83bc417661fcfc92679311e8ba56708f5f7","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"removed","logIndex":"4","observationId":"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"9","txHash":"0x3333333333333333333333333333333333333333333333333333333333333335","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222225","blockNumber":"123459","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222225\",\"blockNumber\":\"123459\",\"chainId\":\"8453\",\"commitment\":\"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"cursorId\":\"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"5\",\"observationId\":\"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df\",\"occurredAt\":\"1778640120\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"3\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"10\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333336\",\"uri\":\"fixture://root-commit-valid\"}","chainId":"8453","commitment":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","cursorId":"0xb8db31a7a1ddb0e07d013a615c3ef49b2cba42cdcebe2a2a06f6e4a22eb9fa40","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"5","observationId":"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df","occurredAt":"1778640120","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"3","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"10","txHash":"0x3333333333333333333333333333333333333333333333333333333333333336","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222226","blockNumber":"123460","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222226\",\"blockNumber\":\"123460\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"6\",\"observationId\":\"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4\",\"occurredAt\":\"1778640180\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004\",\"pulseType\":\"2\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"4\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"11\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333337\",\"uri\":\"fixture://missing-artifact\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x4d24ca0c21db1d275798b2d97af1943227a534953763586ee8247c61eb66a7ec","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"6","observationId":"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4","occurredAt":"1778640180","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004","pulseType":"2","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"4","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"11","txHash":"0x3333333333333333333333333333333333333333333333333333333333333337","uri":"fixture://missing-artifact"},{"actor":"0x4444444444444444444444444444444444444444","blockHash":"0x2222222222222222222222222222222222222222222222222222222222222227","blockNumber":"123461","canonicalObservationJson":"{\"actor\":\"0x4444444444444444444444444444444444444444\",\"blockHash\":\"0x2222222222222222222222222222222222222222222222222222222222222227\",\"blockNumber\":\"123461\",\"chainId\":\"8453\",\"commitment\":\"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5\",\"cursorId\":\"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a\",\"emittingContract\":\"0x1111111111111111111111111111111111111111\",\"eventSignature\":\"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43\",\"lifecycleState\":\"pending\",\"logIndex\":\"7\",\"observationId\":\"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98\",\"occurredAt\":\"1778640240\",\"parentPulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"pulseId\":\"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005\",\"pulseType\":\"99\",\"receiptStatus\":\"success\",\"rootfieldId\":\"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":\"5\",\"subject\":\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1\",\"transactionIndex\":\"12\",\"txHash\":\"0x3333333333333333333333333333333333333333333333333333333333333338\",\"uri\":\"fixture://unsupported\"}","chainId":"8453","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","cursorId":"0x900b2701cc193396e418ce0af071bdded26364eac5399946a6e9745decf46e5a","duplicateKind":"unique","emittingContract":"0x1111111111111111111111111111111111111111","eventSignature":"0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43","lifecycleState":"pending","logIndex":"7","observationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","occurredAt":"1778640240","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005","pulseType":"99","receiptStatus":"success","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"5","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","transactionIndex":"12","txHash":"0x3333333333333333333333333333333333333333333333333333333333333338","uri":"fixture://unsupported"}],"pulses":[{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x49c6cd59d1f1916bc5301308be09c14d64127d1566362272dc5aa201ed53bdea","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x8fabbad2f4aae2b67e5dcacc7cd425b7860d4d4453488c6c9770196b5009d795","observationId":"0x62e6ea1899a4dfd0d0a6cf6010ffb98983db20a9755257c8288602d059aa169e","occurredAt":"1778648420","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab001","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab008","pulseType":"4","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"6","subject":"0x1212121212121212121212121212121212121212121212121212121212121212","uri":"fixture://swap-memory-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x4122209ff672fc04b2ec3af31ab1af79813971f86de000aa6534038cc79de6b5","observationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","occurredAt":"1778640000","parentPulseId":"0x0000000000000000000000000000000000000000000000000000000000000000","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseType":"1","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"1","subject":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","uri":"ipfs://bafy-flowmemory-example"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x223d74c971f301d800dd69aa30994bc3fa3089b34b0db1b0a7fc9d4e8d114c79","occurredAt":"1778640060","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab002","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"2","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","observationId":"0xe4a7065f1578c4a232b41f5984942e9b7b07760ce7dfeba77b38eb03173491df","occurredAt":"1778640120","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab003","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"3","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://root-commit-valid"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x92144fc24c81cdd6319598e6e0c58d84e3f18d9ad4a8be33bc956e32c2ae39f4","occurredAt":"1778640180","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab004","pulseType":"2","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"4","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://missing-artifact"},{"actor":"0x4444444444444444444444444444444444444444","commitment":"0x69a9f953179a6ce2e08724dd759fb71b08b0759a5900fc0399a753ecf0557df5","observationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","occurredAt":"1778640240","parentPulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pulseId":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab005","pulseType":"99","rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","sequence":"5","subject":"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1","uri":"fixture://unsupported"}],"rejectedLogs":[{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222228","blockNumber":"123462","chainId":"8453","logIndex":"8","message":"receipt status is reverted","reasonCode":"receipt.reverted","source":"indexer","transactionIndex":"13","txHash":"0x3333333333333333333333333333333333333333333333333333333333333339"},{"blockHash":"0x2222222222222222222222222222222222222222222222222222222222222229","blockNumber":"123463","chainId":"8453","logIndex":"9","message":"unsupported event signature: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","reasonCode":"log.malformed","source":"indexer","transactionIndex":"14","txHash":"0x3333333333333333333333333333333333333333333333333333333333333340"}],"rootfields":[{"firstObservationId":"0x9d958aadf8bf46f989b51e541709a73d21970e7e79643f939c9a0000b50f9a91","latestObservationId":"0x5ba8c5a70c814d35482df5f6598e549f83f2d0e27f6e3d25b83eb2a0e1d56a98","pulseCount":7,"rootfieldId":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}],"schema":"flowmemory.indexer.state.v0","source":"fixture"}} diff --git a/services/indexer/src/base-canary.ts b/services/indexer/src/base-canary.ts index cd7a2aa9..6aa81b60 100644 --- a/services/indexer/src/base-canary.ts +++ b/services/indexer/src/base-canary.ts @@ -11,6 +11,7 @@ import { blockArgumentToDecimalString, blockArgumentToRpcQuantity, normalizeEvmAddresses, + normalizeRpcUrl, readArgValue, } from "./reader-utils.ts"; import { BASE_MAINNET_CHAIN_ID, readBaseMainnetCanaryFlowPulseLogs } from "./rpc.ts"; @@ -102,15 +103,13 @@ export function parseBaseCanaryReaderArgs(args: string[]): CliOptions { } } - if (rpcUrl.trim() === "") { - throw new Error("--rpc-url is required; FlowMemory does not ship a default RPC endpoint"); - } if (fromBlock.trim() === "") { throw new Error("--from-block is required"); } if (toBlock.trim() === "") { throw new Error("--to-block is required"); } + const normalizedRpcUrl = normalizeRpcUrl(rpcUrl); assertCanaryAcknowledged(acknowledgeMainnetCanary); @@ -119,7 +118,7 @@ export function parseBaseCanaryReaderArgs(args: string[]): CliOptions { assertCanaryBlockRange(normalizedFromBlock, normalizedToBlock); return { - rpcUrl, + rpcUrl: normalizedRpcUrl, addresses: normalizeEvmAddresses(addresses), fromBlock: normalizedFromBlock, toBlock: normalizedToBlock, @@ -133,6 +132,7 @@ export function parseBaseCanaryReaderArgs(args: string[]): CliOptions { export async function runBaseCanaryReader(options: BaseCanaryReaderOptions): Promise { assertCanaryAcknowledged(options.acknowledgeMainnetCanary); + const rpcUrl = normalizeRpcUrl(options.rpcUrl); const addresses = normalizeEvmAddresses(options.addresses); const fromBlock = blockArgumentToDecimalString(options.fromBlock); const toBlock = blockArgumentToDecimalString(options.toBlock); @@ -142,7 +142,7 @@ export async function runBaseCanaryReader(options: BaseCanaryReaderOptions): Pro assertCanaryBlockRange(fromBlock, toBlock); const readResult = await readBaseMainnetCanaryFlowPulseLogs({ - rpcUrl: options.rpcUrl, + rpcUrl, addresses, fromBlock: blockArgumentToRpcQuantity(fromBlock), toBlock: blockArgumentToRpcQuantity(toBlock), @@ -158,6 +158,7 @@ export async function runBaseCanaryReader(options: BaseCanaryReaderOptions): Pro finalizedBlockNumber, source: "base-mainnet-canary-rpc", sourceAddresses: addresses, + preRejectedLogs: readResult.rejectedLogs, }); const checkpoint = baseCanaryIndexerCheckpoint({ addresses, @@ -203,7 +204,10 @@ if (process.argv[1]?.replaceAll("\\", "/").endsWith("/base-canary.ts")) { observationCount: result.checkpoint.observationCount, rejectedLogCount: result.checkpoint.rejectedLogCount, duplicateCount: result.checkpoint.duplicateCount, + dashboardCanonicalObservationCount: result.checkpoint.dashboardFeed.dashboardCanonicalObservationCount, lastIndexedBlock: result.checkpoint.lastIndexedBlock, + lastScannedBlock: result.checkpoint.lastScannedBlock, + hasIntegrityWarnings: result.checkpoint.dashboardFeed.hasIntegrityWarnings, productionReady: result.checkpoint.safety.productionReady, }, null, 2)); }) diff --git a/services/indexer/src/base-sepolia.ts b/services/indexer/src/base-sepolia.ts index ca8787f5..2ecf2e77 100644 --- a/services/indexer/src/base-sepolia.ts +++ b/services/indexer/src/base-sepolia.ts @@ -3,6 +3,7 @@ import { resolve } from "node:path"; import { indexFlowPulseLogs, type IndexerState } from "./indexer.ts"; import { baseSepoliaIndexerCheckpoint, + readBaseSepoliaIndexerCheckpoint, type BaseSepoliaIndexerCheckpoint, writeBaseSepoliaIndexerCheckpoint, writeIndexerState, @@ -11,20 +12,25 @@ import { blockArgumentToDecimalString, blockArgumentToRpcQuantity, normalizeEvmAddresses, + normalizeRpcUrl, readArgValue, } from "./reader-utils.ts"; import { BASE_SEPOLIA_CHAIN_ID, readBaseSepoliaFlowPulseLogs } from "./rpc.ts"; export { blockArgumentToDecimalString, blockArgumentToRpcQuantity } from "./reader-utils.ts"; +export const BASE_SEPOLIA_MAX_BLOCK_SPAN = 10_000n; + export interface BaseSepoliaReaderOptions { rpcUrl: string; addresses: string[]; - fromBlock: string; + fromBlock?: string; toBlock: string; outPath?: string; checkpointPath?: string; finalizedBlockNumber?: string; + resumeFromCheckpoint?: boolean; + maxBlockSpan?: string | bigint; generatedAt?: string; fetchImpl?: typeof fetch; } @@ -37,8 +43,52 @@ export interface BaseSepoliaReaderResult { } interface CliOptions extends BaseSepoliaReaderOptions { + fromBlock: string; outPath: string; checkpointPath: string; + maxBlockSpan: string; +} + +function normalizeMaxBlockSpan(value?: string | bigint): bigint { + if (value === undefined) return BASE_SEPOLIA_MAX_BLOCK_SPAN; + const normalized = typeof value === "bigint" ? value : BigInt(blockArgumentToDecimalString(value)); + if (normalized < 0n) { + throw new Error("--max-block-span must be non-negative"); + } + return normalized; +} + +function assertBaseSepoliaBlockRange(fromBlock: string, toBlock: string, maxBlockSpan?: string | bigint): void { + if (BigInt(toBlock) < BigInt(fromBlock)) { + throw new Error("--to-block must be greater than or equal to --from-block"); + } + + const span = BigInt(toBlock) - BigInt(fromBlock); + const limit = normalizeMaxBlockSpan(maxBlockSpan); + if (span > limit) { + throw new Error(`Base Sepolia reader refuses broad scans; block span ${span.toString()} exceeds ${limit.toString()}`); + } +} + +function resolveFromBlock(input: { + explicitFromBlock?: string; + checkpointPath: string; + resumeFromCheckpoint?: boolean; +}): string { + if (input.explicitFromBlock !== undefined && input.explicitFromBlock.trim() !== "") { + return blockArgumentToDecimalString(input.explicitFromBlock); + } + if (input.resumeFromCheckpoint === true) { + try { + const checkpoint = readBaseSepoliaIndexerCheckpoint(input.checkpointPath); + return checkpoint.nextFromBlock; + } catch (error) { + throw new Error( + `--resume-from-checkpoint requires an existing Base Sepolia checkpoint or explicit --from-block (${input.checkpointPath})`, + ); + } + } + throw new Error("--from-block is required"); } export function parseBaseSepoliaReaderArgs(args: string[]): CliOptions { @@ -46,6 +96,8 @@ export function parseBaseSepoliaReaderArgs(args: string[]): CliOptions { let fromBlock = ""; let toBlock = ""; let finalizedBlockNumber: string | undefined; + let resumeFromCheckpoint = false; + let maxBlockSpan = BASE_SEPOLIA_MAX_BLOCK_SPAN.toString(); const addresses: string[] = []; let outPath = "out/base-sepolia-indexer-state.json"; let checkpointPath = "out/base-sepolia-indexer-checkpoint.json"; @@ -67,6 +119,11 @@ export function parseBaseSepoliaReaderArgs(args: string[]): CliOptions { } else if (arg === "--finalized-block") { finalizedBlockNumber = blockArgumentToDecimalString(readArgValue(args, index, arg)); index += 1; + } else if (arg === "--resume-from-checkpoint") { + resumeFromCheckpoint = true; + } else if (arg === "--max-block-span") { + maxBlockSpan = blockArgumentToDecimalString(readArgValue(args, index, arg)); + index += 1; } else if (arg === "--out") { outPath = readArgValue(args, index, arg); index += 1; @@ -78,40 +135,54 @@ export function parseBaseSepoliaReaderArgs(args: string[]): CliOptions { } } - if (rpcUrl.trim() === "") { - throw new Error("--rpc-url is required; FlowMemory does not ship a default RPC endpoint"); - } - if (fromBlock.trim() === "") { - throw new Error("--from-block is required"); - } if (toBlock.trim() === "") { throw new Error("--to-block is required"); } + const normalizedRpcUrl = normalizeRpcUrl(rpcUrl); + + const normalizedFromBlock = resolveFromBlock({ + explicitFromBlock: fromBlock, + checkpointPath: resolve(checkpointPath), + resumeFromCheckpoint, + }); + const normalizedToBlock = blockArgumentToDecimalString(toBlock); + assertBaseSepoliaBlockRange(normalizedFromBlock, normalizedToBlock, maxBlockSpan); + if (finalizedBlockNumber !== undefined && BigInt(finalizedBlockNumber) > BigInt(normalizedToBlock)) { + throw new Error("--finalized-block must be less than or equal to --to-block"); + } return { - rpcUrl, + rpcUrl: normalizedRpcUrl, addresses: normalizeEvmAddresses(addresses), - fromBlock: blockArgumentToDecimalString(fromBlock), - toBlock: blockArgumentToDecimalString(toBlock), + fromBlock: normalizedFromBlock, + toBlock: normalizedToBlock, finalizedBlockNumber, + resumeFromCheckpoint, + maxBlockSpan, outPath, checkpointPath, }; } export async function runBaseSepoliaReader(options: BaseSepoliaReaderOptions): Promise { + const rpcUrl = normalizeRpcUrl(options.rpcUrl); const addresses = normalizeEvmAddresses(options.addresses); - const fromBlock = blockArgumentToDecimalString(options.fromBlock); const toBlock = blockArgumentToDecimalString(options.toBlock); const outPath = resolve(options.outPath ?? "out/base-sepolia-indexer-state.json"); const checkpointPath = resolve(options.checkpointPath ?? "out/base-sepolia-indexer-checkpoint.json"); + const fromBlock = resolveFromBlock({ + explicitFromBlock: options.fromBlock, + checkpointPath, + resumeFromCheckpoint: options.resumeFromCheckpoint, + }); - if (BigInt(toBlock) < BigInt(fromBlock)) { - throw new Error("--to-block must be greater than or equal to --from-block"); + assertBaseSepoliaBlockRange(fromBlock, toBlock, options.maxBlockSpan); + if (options.finalizedBlockNumber !== undefined && BigInt(blockArgumentToDecimalString(options.finalizedBlockNumber)) > BigInt(toBlock)) { + throw new Error("--finalized-block must be less than or equal to --to-block"); } const readResult = await readBaseSepoliaFlowPulseLogs({ - rpcUrl: options.rpcUrl, + rpcUrl, addresses, fromBlock: blockArgumentToRpcQuantity(fromBlock), toBlock: blockArgumentToRpcQuantity(toBlock), @@ -127,6 +198,7 @@ export async function runBaseSepoliaReader(options: BaseSepoliaReaderOptions): P finalizedBlockNumber, source: "base-sepolia-rpc", sourceAddresses: addresses, + preRejectedLogs: readResult.rejectedLogs, }); const checkpoint = baseSepoliaIndexerCheckpoint({ addresses, @@ -170,7 +242,13 @@ if (process.argv[1]?.replaceAll("\\", "/").endsWith("/base-sepolia.ts")) { checkpointPath: result.checkpointPath, observationCount: result.checkpoint.observationCount, rejectedLogCount: result.checkpoint.rejectedLogCount, + duplicateCount: result.checkpoint.duplicateCount, + dashboardCanonicalObservationCount: result.checkpoint.dashboardFeed.dashboardCanonicalObservationCount, lastIndexedBlock: result.checkpoint.lastIndexedBlock, + lastScannedBlock: result.checkpoint.lastScannedBlock, + nextFromBlock: result.checkpoint.nextFromBlock, + emptyRange: result.checkpoint.emptyRange, + hasIntegrityWarnings: result.checkpoint.dashboardFeed.hasIntegrityWarnings, }, null, 2)); }) .catch((error) => { diff --git a/services/indexer/src/indexer.ts b/services/indexer/src/indexer.ts index 9d4ebf75..ff31014e 100644 --- a/services/indexer/src/indexer.ts +++ b/services/indexer/src/indexer.ts @@ -29,6 +29,9 @@ export interface IndexRejectedLog { logIndex: string; reasonCode: string; message: string; + source?: "indexer" | "rpc"; + rawLogIndex?: number; + address?: string; } export interface IndexedCursor { @@ -73,6 +76,42 @@ export interface IndexedRootfield { export type IndexerStateSource = "fixture" | "local-rpc-placeholder" | "base-sepolia-rpc" | "base-mainnet-canary-rpc"; +export interface IndexerDashboardFeedObservation { + observationId: string; + pulseId: string; + rootfieldId: string; + lifecycleState: ParsedFlowPulseObservation["lifecycleState"]; + duplicateKind: DuplicateKind; + dashboardCanonical: boolean; + chainId: string; + emittingContract: string; + blockNumber: string; + blockHash: string; + txHash: string; + transactionIndex: string; + logIndex: string; + pulseType: string; + sequence: string; + occurredAt: string; + uri: string; +} + +export interface IndexerDashboardFeed { + schema: "flowmemory.indexer.dashboard_feed.v0"; + source: IndexerStateSource; + chainId: string; + sourceSetId: string; + observationCount: number; + dashboardCanonicalObservationCount: number; + rejectedLogCount: number; + duplicateCount: number; + duplicateKindCounts: Record; + rejectedReasonCounts: Record; + warningCodes: string[]; + hasIntegrityWarnings: boolean; + observations: IndexerDashboardFeedObservation[]; +} + export interface IndexerState { schema: "flowmemory.indexer.state.v0"; source: IndexerStateSource; @@ -87,6 +126,7 @@ export interface IndexerState { observationId: string; pulseId: string; }>; + dashboardFeed: IndexerDashboardFeed; } export interface IndexerStateOptions { @@ -95,6 +135,7 @@ export interface IndexerStateOptions { chainId?: string; source?: IndexerStateSource; sourceAddresses?: string[]; + preRejectedLogs?: IndexRejectedLog[]; } function canonicalObservationJson(observation: ParsedFlowPulseObservation): string { @@ -166,17 +207,106 @@ function duplicateKindFor( return "unique"; } +function sortedObservationsForFeed(observations: IndexedObservation[]): IndexedObservation[] { + return [...observations].sort((left, right) => { + const block = BigInt(left.blockNumber) - BigInt(right.blockNumber); + if (block !== 0n) return block < 0n ? -1 : 1; + const transaction = BigInt(left.transactionIndex) - BigInt(right.transactionIndex); + if (transaction !== 0n) return transaction < 0n ? -1 : 1; + const log = BigInt(left.logIndex) - BigInt(right.logIndex); + if (log !== 0n) return log < 0n ? -1 : 1; + return left.observationId.localeCompare(right.observationId); + }); +} + +function incrementCount(counts: Record, key: string): void { + counts[key] = (counts[key] ?? 0) + 1; +} + +function buildDashboardFeed(input: { + source: IndexerStateSource; + chainId: string; + sourceSetId: string; + observations: IndexedObservation[]; + rejectedLogs: IndexRejectedLog[]; + duplicates: IndexerState["duplicates"]; +}): IndexerDashboardFeed { + const duplicateKindCounts: Record = {}; + const rejectedReasonCounts: Record = {}; + + for (const duplicate of input.duplicates) { + incrementCount(duplicateKindCounts, duplicate.kind); + } + for (const rejected of input.rejectedLogs) { + incrementCount(rejectedReasonCounts, rejected.reasonCode); + } + + const warningCodes = new Set(); + for (const rejected of input.rejectedLogs) { + warningCodes.add(`rejected.${rejected.reasonCode}`); + } + for (const duplicate of input.duplicates) { + warningCodes.add(`duplicate.${duplicate.kind}`); + } + for (const observation of input.observations) { + if (observation.lifecycleState === "removed" || observation.lifecycleState === "reorged") { + warningCodes.add(`lifecycle.${observation.lifecycleState}`); + } + } + + const observations = sortedObservationsForFeed(input.observations).map((observation) => ({ + observationId: observation.observationId, + pulseId: observation.pulseId, + rootfieldId: observation.rootfieldId, + lifecycleState: observation.lifecycleState, + duplicateKind: observation.duplicateKind, + dashboardCanonical: + observation.duplicateKind !== "exactDuplicate" && + observation.lifecycleState !== "removed" && + observation.lifecycleState !== "reorged" && + observation.lifecycleState !== "superseded", + chainId: observation.chainId, + emittingContract: observation.emittingContract, + blockNumber: observation.blockNumber, + blockHash: observation.blockHash, + txHash: observation.txHash, + transactionIndex: observation.transactionIndex, + logIndex: observation.logIndex, + pulseType: observation.pulseType, + sequence: observation.sequence, + occurredAt: observation.occurredAt, + uri: observation.uri, + })); + + return { + schema: "flowmemory.indexer.dashboard_feed.v0", + source: input.source, + chainId: input.chainId, + sourceSetId: input.sourceSetId, + observationCount: input.observations.length, + dashboardCanonicalObservationCount: observations.filter((observation) => observation.dashboardCanonical).length, + rejectedLogCount: input.rejectedLogs.length, + duplicateCount: input.duplicates.length, + duplicateKindCounts, + rejectedReasonCounts, + warningCodes: [...warningCodes].sort(), + hasIntegrityWarnings: warningCodes.size > 0, + observations, + }; +} + export function indexFlowPulseLogs(logs: RawFlowPulseLogFixture[], options: IndexerStateOptions = {}): IndexerState { const seenByObservationId = new Map(); const seenByPulseId = new Map(); const rootfields = new Map(); const observations: IndexedObservation[] = []; const cursors = new Map(); - const rejectedLogs: IndexRejectedLog[] = []; + const rejectedLogs: IndexRejectedLog[] = [...(options.preRejectedLogs ?? [])]; const duplicates: IndexerState["duplicates"] = []; const source = options.source ?? "fixture"; const sourceAddresses = options.sourceAddresses ?? logs.map((log) => log.address); const sourceSetId = deriveSourceSetId(logs[0]?.chainId ?? options.chainId ?? "0", sourceAddresses); + const chainId = logs[0]?.chainId ?? options.chainId ?? "0"; for (const log of logs) { if (log.receiptStatus !== "success") { @@ -189,6 +319,7 @@ export function indexFlowPulseLogs(logs: RawFlowPulseLogFixture[], options: Inde logIndex: log.logIndex, reasonCode: "receipt.reverted", message: "receipt status is reverted", + source: "indexer", }); continue; } @@ -207,6 +338,7 @@ export function indexFlowPulseLogs(logs: RawFlowPulseLogFixture[], options: Inde logIndex: log.logIndex, reasonCode: "log.malformed", message: error instanceof Error ? error.message : "unknown parse error", + source: "indexer", }); continue; } @@ -262,6 +394,15 @@ export function indexFlowPulseLogs(logs: RawFlowPulseLogFixture[], options: Inde } } + const dashboardFeed = buildDashboardFeed({ + source, + chainId, + sourceSetId, + observations, + rejectedLogs, + duplicates, + }); + return { schema: "flowmemory.indexer.state.v0", source, @@ -291,6 +432,7 @@ export function indexFlowPulseLogs(logs: RawFlowPulseLogFixture[], options: Inde rootfields: [...rootfields.values()].sort((left, right) => left.rootfieldId.localeCompare(right.rootfieldId)), rejectedLogs, duplicates, + dashboardFeed, }; } diff --git a/services/indexer/src/persistence.ts b/services/indexer/src/persistence.ts index a40fe53c..35fcdfbd 100644 --- a/services/indexer/src/persistence.ts +++ b/services/indexer/src/persistence.ts @@ -1,14 +1,28 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname } from "node:path"; +import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; -import { canonicalJson } from "../../shared/src/index.ts"; -import type { IndexerState } from "./indexer.ts"; +import { assertNoSecrets, canonicalJson, keccak256Hex } from "../../shared/src/index.ts"; +import type { IndexerDashboardFeed, IndexerState } from "./indexer.ts"; export interface PersistedIndexerState { schema: "flowmemory.indexer.persistence.v0"; state: IndexerState; } +export interface CheckpointDashboardFeed { + schema: "flowmemory.indexer.checkpoint_dashboard_feed.v0"; + feedSchema: IndexerDashboardFeed["schema"]; + sourceSetId: string; + observationCount: number; + dashboardCanonicalObservationCount: number; + rejectedLogCount: number; + duplicateCount: number; + rejectedReasonCounts: Record; + duplicateKindCounts: Record; + warningCodes: string[]; + hasIntegrityWarnings: boolean; +} + export interface BaseSepoliaIndexerCheckpoint { schema: "flowmemory.indexer.base_sepolia_checkpoint.v0"; network: "base-sepolia"; @@ -24,7 +38,19 @@ export interface BaseSepoliaIndexerCheckpoint { rejectedLogCount: number; duplicateCount: number; lastIndexedBlock: string; + lastScannedBlock: string; + highestObservedBlock: string | null; + nextFromBlock: string; + emptyRange: boolean; + stateDigest: `0x${string}`; generatedAt: string; + dashboardFeed: CheckpointDashboardFeed; + safety: { + networkBoundary: "base-sepolia-testnet-only"; + productionReady: false; + storesRpcUrl: false; + storesPrivateKeys: false; + }; } export interface BaseCanaryIndexerCheckpoint { @@ -42,10 +68,18 @@ export interface BaseCanaryIndexerCheckpoint { rejectedLogCount: number; duplicateCount: number; lastIndexedBlock: string; + lastScannedBlock: string; + highestObservedBlock: string | null; + nextFromBlock: string; + emptyRange: boolean; + stateDigest: `0x${string}`; generatedAt: string; + dashboardFeed: CheckpointDashboardFeed; safety: { acknowledgement: "base-mainnet-canary-only"; productionReady: false; + storesRpcUrl: false; + storesPrivateKeys: false; }; } @@ -56,6 +90,26 @@ export function persistedIndexerState(state: IndexerState): PersistedIndexerStat }; } +export function indexerStateDigest(state: IndexerState): `0x${string}` { + return keccak256Hex(new TextEncoder().encode(canonicalJson(persistedIndexerState(state)))); +} + +function checkpointDashboardFeed(state: IndexerState): CheckpointDashboardFeed { + return { + schema: "flowmemory.indexer.checkpoint_dashboard_feed.v0", + feedSchema: state.dashboardFeed.schema, + sourceSetId: state.dashboardFeed.sourceSetId, + observationCount: state.dashboardFeed.observationCount, + dashboardCanonicalObservationCount: state.dashboardFeed.dashboardCanonicalObservationCount, + rejectedLogCount: state.dashboardFeed.rejectedLogCount, + duplicateCount: state.dashboardFeed.duplicateCount, + rejectedReasonCounts: state.dashboardFeed.rejectedReasonCounts, + duplicateKindCounts: state.dashboardFeed.duplicateKindCounts, + warningCodes: state.dashboardFeed.warningCodes, + hasIntegrityWarnings: state.dashboardFeed.hasIntegrityWarnings, + }; +} + export function baseSepoliaIndexerCheckpoint(input: { addresses: string[]; fromBlock: string; @@ -65,9 +119,11 @@ export function baseSepoliaIndexerCheckpoint(input: { state: IndexerState; generatedAt?: string; }): BaseSepoliaIndexerCheckpoint { - const lastIndexedBlock = input.state.cursors.reduce((latest, cursor) => { + const highestObservedBlock = input.state.cursors.reduce((latest, cursor) => { + if (latest === null) return cursor.blockNumber; return BigInt(cursor.blockNumber) > BigInt(latest) ? cursor.blockNumber : latest; - }, input.fromBlock); + }, null); + const lastIndexedBlock = input.toBlock; return { schema: "flowmemory.indexer.base_sepolia_checkpoint.v0", @@ -84,7 +140,19 @@ export function baseSepoliaIndexerCheckpoint(input: { rejectedLogCount: input.state.rejectedLogs.length, duplicateCount: input.state.duplicates.length, lastIndexedBlock, + lastScannedBlock: input.toBlock, + highestObservedBlock, + nextFromBlock: (BigInt(lastIndexedBlock) + 1n).toString(), + emptyRange: input.state.observations.length === 0 && input.state.rejectedLogs.length === 0, + stateDigest: indexerStateDigest(input.state), generatedAt: input.generatedAt ?? new Date().toISOString(), + dashboardFeed: checkpointDashboardFeed(input.state), + safety: { + networkBoundary: "base-sepolia-testnet-only", + productionReady: false, + storesRpcUrl: false, + storesPrivateKeys: false, + }, }; } @@ -97,9 +165,11 @@ export function baseCanaryIndexerCheckpoint(input: { state: IndexerState; generatedAt?: string; }): BaseCanaryIndexerCheckpoint { - const lastIndexedBlock = input.state.cursors.reduce((latest, cursor) => { + const highestObservedBlock = input.state.cursors.reduce((latest, cursor) => { + if (latest === null) return cursor.blockNumber; return BigInt(cursor.blockNumber) > BigInt(latest) ? cursor.blockNumber : latest; - }, input.fromBlock); + }, null); + const lastIndexedBlock = input.toBlock; return { schema: "flowmemory.indexer.base_canary_checkpoint.v0", @@ -116,17 +186,34 @@ export function baseCanaryIndexerCheckpoint(input: { rejectedLogCount: input.state.rejectedLogs.length, duplicateCount: input.state.duplicates.length, lastIndexedBlock, + lastScannedBlock: input.toBlock, + highestObservedBlock, + nextFromBlock: (BigInt(lastIndexedBlock) + 1n).toString(), + emptyRange: input.state.observations.length === 0 && input.state.rejectedLogs.length === 0, + stateDigest: indexerStateDigest(input.state), generatedAt: input.generatedAt ?? new Date().toISOString(), + dashboardFeed: checkpointDashboardFeed(input.state), safety: { acknowledgement: "base-mainnet-canary-only", productionReady: false, + storesRpcUrl: false, + storesPrivateKeys: false, }, }; } -export function writeIndexerState(path: string, state: IndexerState): void { +function writeCanonicalJsonFile(path: string, value: unknown, scanForSecrets = false): void { + if (scanForSecrets) { + assertNoSecrets(value); + } mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, `${canonicalJson(persistedIndexerState(state))}\n`, "utf8"); + const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.tmp`); + writeFileSync(tempPath, `${canonicalJson(value)}\n`, "utf8"); + renameSync(tempPath, path); +} + +export function writeIndexerState(path: string, state: IndexerState): void { + writeCanonicalJsonFile(path, persistedIndexerState(state)); } export function readIndexerState(path: string): PersistedIndexerState { @@ -134,8 +221,7 @@ export function readIndexerState(path: string): PersistedIndexerState { } export function writeBaseSepoliaIndexerCheckpoint(path: string, checkpoint: BaseSepoliaIndexerCheckpoint): void { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, `${canonicalJson(checkpoint)}\n`, "utf8"); + writeCanonicalJsonFile(path, checkpoint, true); } export function readBaseSepoliaIndexerCheckpoint(path: string): BaseSepoliaIndexerCheckpoint { @@ -143,8 +229,7 @@ export function readBaseSepoliaIndexerCheckpoint(path: string): BaseSepoliaIndex } export function writeBaseCanaryIndexerCheckpoint(path: string, checkpoint: BaseCanaryIndexerCheckpoint): void { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, `${canonicalJson(checkpoint)}\n`, "utf8"); + writeCanonicalJsonFile(path, checkpoint, true); } export function readBaseCanaryIndexerCheckpoint(path: string): BaseCanaryIndexerCheckpoint { diff --git a/services/indexer/src/reader-utils.ts b/services/indexer/src/reader-utils.ts index 621385cd..9c6b7a90 100644 --- a/services/indexer/src/reader-utils.ts +++ b/services/indexer/src/reader-utils.ts @@ -1,3 +1,39 @@ +import { findSecret } from "../../shared/src/index.ts"; + +export function normalizeRpcUrl(rpcUrl: string): string { + const trimmed = rpcUrl.trim(); + if (trimmed === "") { + throw new Error("--rpc-url is required; FlowMemory does not ship a default RPC endpoint"); + } + if (/[\s\x00-\x1f\x7f]/.test(trimmed)) { + throw new Error("--rpc-url must be a single explicit URL without whitespace or control characters"); + } + if (/^(\$|\$\{|%).+/.test(trimmed)) { + throw new Error("--rpc-url must be a resolved URL, not an environment variable placeholder"); + } + + let parsed: URL; + try { + parsed = new URL(trimmed); + } catch { + throw new Error("--rpc-url must be an absolute http(s) URL"); + } + + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + throw new Error("--rpc-url must use http or https"); + } + if (parsed.username !== "" || parsed.password !== "") { + throw new Error("--rpc-url must not include username/password credentials"); + } + + const secret = findSecret(trimmed); + if (secret !== null) { + throw new Error(`--rpc-url contains secret-shaped material: ${secret.reasonCode}`); + } + + return trimmed; +} + export function normalizeEvmAddress(address: string): string { const normalized = address.trim().toLowerCase(); if (!/^0x[0-9a-f]{40}$/.test(normalized)) { @@ -30,6 +66,25 @@ export function blockArgumentToRpcQuantity(value: string): string { return `0x${BigInt(blockArgumentToDecimalString(value)).toString(16)}`; } +export function assertRpcQuantity(value: string, name: string): void { + if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(value)) { + throw new Error(`${name} must be a normalized JSON-RPC quantity`); + } +} + +export function assertBlockRange(fromBlock: string, toBlock: string, maxSpan?: bigint): void { + if (BigInt(toBlock) < BigInt(fromBlock)) { + throw new Error("--to-block must be greater than or equal to --from-block"); + } + + if (maxSpan !== undefined) { + const span = BigInt(toBlock) - BigInt(fromBlock); + if (span > maxSpan) { + throw new Error(`reader refuses broad scans; block span ${span.toString()} exceeds ${maxSpan.toString()}`); + } + } +} + export function readArgValue(args: string[], index: number, name: string): string { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) { diff --git a/services/indexer/src/rpc.ts b/services/indexer/src/rpc.ts index 9be21dc8..518908a2 100644 --- a/services/indexer/src/rpc.ts +++ b/services/indexer/src/rpc.ts @@ -1,4 +1,11 @@ import { FLOWPULSE_EVENT_TOPIC0, type RawFlowPulseLogFixture } from "../../shared/src/index.ts"; +import type { IndexRejectedLog } from "./indexer.ts"; +import { + assertBlockRange, + assertRpcQuantity, + normalizeEvmAddresses, + normalizeRpcUrl, +} from "./reader-utils.ts"; export const BASE_MAINNET_CHAIN_ID = "8453"; export const BASE_SEPOLIA_CHAIN_ID = "84532"; @@ -14,6 +21,7 @@ export interface LocalRpcReadOptions { export interface RpcFlowPulseReadResult { chainId: string; logs: RawFlowPulseLogFixture[]; + rejectedLogs: IndexRejectedLog[]; } interface JsonRpcResponse { @@ -42,6 +50,67 @@ interface RpcReceipt { status: string; } +interface NormalizedRpcReadOptions extends LocalRpcReadOptions { + rpcUrl: string; + addresses: string[]; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function stringField(value: Record, key: string): string | null { + const field = value[key]; + return typeof field === "string" ? field : null; +} + +function optionalBooleanField(value: Record, key: string): boolean | undefined { + const field = value[key]; + if (field === undefined) return undefined; + return typeof field === "boolean" ? field : undefined; +} + +function safeString(value: unknown, fallback = "unknown"): string { + return typeof value === "string" && value.trim() !== "" ? value : fallback; +} + +function rejectedRpcLog(input: { + chainId: string; + rawLog: unknown; + rawLogIndex: number; + reasonCode: string; + message: string; +}): IndexRejectedLog { + const rawLog = isRecord(input.rawLog) ? input.rawLog : {}; + return { + chainId: input.chainId, + blockNumber: safeString(rawLog.blockNumber), + blockHash: safeString(rawLog.blockHash), + txHash: safeString(rawLog.transactionHash), + transactionIndex: safeString(rawLog.transactionIndex), + logIndex: safeString(rawLog.logIndex), + reasonCode: input.reasonCode, + message: input.message, + source: "rpc", + rawLogIndex: input.rawLogIndex, + address: safeString(rawLog.address), + }; +} + +function normalizeRpcReadOptions(options: LocalRpcReadOptions): NormalizedRpcReadOptions { + const rpcUrl = normalizeRpcUrl(options.rpcUrl); + const addresses = normalizeEvmAddresses(options.addresses); + assertRpcQuantity(options.fromBlock, "fromBlock"); + assertRpcQuantity(options.toBlock, "toBlock"); + assertBlockRange(quantityToDecimalString(options.fromBlock), quantityToDecimalString(options.toBlock)); + + return { + ...options, + rpcUrl, + addresses, + }; +} + function quantityToDecimalString(quantity: string): string { if (!/^0x[0-9a-fA-F]+$/.test(quantity)) { throw new Error(`invalid JSON-RPC quantity: ${quantity}`); @@ -66,8 +135,15 @@ async function rpc(fetchImpl: typeof fetch, rpcUrl: string, method: string, p } const payload = await response.json() as JsonRpcResponse; + if (!isRecord(payload) || payload.jsonrpc !== "2.0") { + throw new Error(`JSON-RPC ${method} returned a malformed envelope`); + } if (payload.error !== undefined) { - throw new Error(`JSON-RPC error ${payload.error.code}: ${payload.error.message}`); + const error = payload.error; + if (!isRecord(error) || typeof error.code !== "number" || typeof error.message !== "string") { + throw new Error(`JSON-RPC ${method} returned a malformed error`); + } + throw new Error(`JSON-RPC error ${error.code}: ${error.message}`); } if (payload.result === undefined) { throw new Error(`JSON-RPC ${method} returned no result`); @@ -81,38 +157,130 @@ async function readRpcChainId(fetchImpl: typeof fetch, rpcUrl: string): Promise< } async function readRpcFlowPulseLogSetWithChainId( - options: LocalRpcReadOptions, + options: NormalizedRpcReadOptions, chainId: string, fetchImpl: typeof fetch, ): Promise { - const logs = await rpc(fetchImpl, options.rpcUrl, "eth_getLogs", [{ + const logs = await rpc(fetchImpl, options.rpcUrl, "eth_getLogs", [{ address: options.addresses, fromBlock: options.fromBlock, toBlock: options.toBlock, topics: [FLOWPULSE_EVENT_TOPIC0], }]); + if (!Array.isArray(logs)) { + throw new Error("JSON-RPC eth_getLogs result must be an array"); + } const rawLogs: RawFlowPulseLogFixture[] = []; - for (const log of logs) { - const receipt = await rpc(fetchImpl, options.rpcUrl, "eth_getTransactionReceipt", [log.transactionHash]); + const rejectedLogs: IndexRejectedLog[] = []; + for (let rawLogIndex = 0; rawLogIndex < logs.length; rawLogIndex += 1) { + const log = logs[rawLogIndex]; + if (!isRecord(log)) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.log.malformed", + message: "eth_getLogs result item is not an object", + })); + continue; + } + + const address = stringField(log, "address"); + const topics = log.topics; + const data = stringField(log, "data"); + const blockNumber = stringField(log, "blockNumber"); + const blockHash = stringField(log, "blockHash"); + const transactionHash = stringField(log, "transactionHash"); + const transactionIndex = stringField(log, "transactionIndex"); + const logIndex = stringField(log, "logIndex"); + const removed = optionalBooleanField(log, "removed"); + + if (!Array.isArray(topics) || !topics.every((topic) => typeof topic === "string")) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.log.malformed", + message: "eth_getLogs log topics must be an array of strings", + })); + continue; + } + + const requiredFields = { address, data, blockNumber, blockHash, transactionHash, transactionIndex, logIndex }; + const missingField = Object.entries(requiredFields).find(([, value]) => value === null)?.[0]; + if (missingField !== undefined) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.log.malformed", + message: `eth_getLogs log is missing string field ${missingField}`, + })); + continue; + } + + let normalizedBlockNumber: string; + let normalizedTransactionIndex: string; + let normalizedLogIndex: string; + try { + normalizedBlockNumber = quantityToDecimalString(blockNumber); + normalizedTransactionIndex = quantityToDecimalString(transactionIndex); + normalizedLogIndex = quantityToDecimalString(logIndex); + } catch (error) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.log.malformed", + message: error instanceof Error ? error.message : "invalid RPC log quantity", + })); + continue; + } + + let receipt: RpcReceipt; + try { + receipt = await rpc(fetchImpl, options.rpcUrl, "eth_getTransactionReceipt", [transactionHash]); + } catch (error) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.receipt.unavailable", + message: error instanceof Error ? error.message : "transaction receipt unavailable", + })); + continue; + } + if (!isRecord(receipt) || (receipt.status !== "0x1" && receipt.status !== "0x0")) { + rejectedLogs.push(rejectedRpcLog({ + chainId, + rawLog: log, + rawLogIndex, + reasonCode: "rpc.receipt.malformed", + message: "transaction receipt status must be 0x1 or 0x0", + })); + continue; + } + rawLogs.push({ chainId, - address: log.address, - topics: log.topics, - data: log.data, - blockNumber: quantityToDecimalString(log.blockNumber), - blockHash: log.blockHash, - transactionHash: log.transactionHash, - transactionIndex: quantityToDecimalString(log.transactionIndex), - logIndex: quantityToDecimalString(log.logIndex), + address, + topics, + data, + blockNumber: normalizedBlockNumber, + blockHash, + transactionHash, + transactionIndex: normalizedTransactionIndex, + logIndex: normalizedLogIndex, receiptStatus: receipt.status === "0x1" ? "success" : "reverted", - removed: log.removed, + removed, }); } return { chainId, logs: rawLogs, + rejectedLogs, }; } @@ -122,24 +290,27 @@ export async function readLocalRpcFlowPulseLogs(options: LocalRpcReadOptions): P export async function readRpcFlowPulseLogSet(options: LocalRpcReadOptions): Promise { const fetchImpl = options.fetchImpl ?? fetch; - const chainId = await readRpcChainId(fetchImpl, options.rpcUrl); - return readRpcFlowPulseLogSetWithChainId(options, chainId, fetchImpl); + const normalizedOptions = normalizeRpcReadOptions(options); + const chainId = await readRpcChainId(fetchImpl, normalizedOptions.rpcUrl); + return readRpcFlowPulseLogSetWithChainId(normalizedOptions, chainId, fetchImpl); } export async function readBaseSepoliaFlowPulseLogs(options: LocalRpcReadOptions): Promise { const fetchImpl = options.fetchImpl ?? fetch; - const chainId = await readRpcChainId(fetchImpl, options.rpcUrl); + const normalizedOptions = normalizeRpcReadOptions(options); + const chainId = await readRpcChainId(fetchImpl, normalizedOptions.rpcUrl); if (chainId !== BASE_SEPOLIA_CHAIN_ID) { throw new Error(`expected Base Sepolia chainId ${BASE_SEPOLIA_CHAIN_ID}, received ${chainId}`); } - return readRpcFlowPulseLogSetWithChainId(options, chainId, fetchImpl); + return readRpcFlowPulseLogSetWithChainId(normalizedOptions, chainId, fetchImpl); } export async function readBaseMainnetCanaryFlowPulseLogs(options: LocalRpcReadOptions): Promise { const fetchImpl = options.fetchImpl ?? fetch; - const chainId = await readRpcChainId(fetchImpl, options.rpcUrl); + const normalizedOptions = normalizeRpcReadOptions(options); + const chainId = await readRpcChainId(fetchImpl, normalizedOptions.rpcUrl); if (chainId !== BASE_MAINNET_CHAIN_ID) { throw new Error(`expected Base mainnet chainId ${BASE_MAINNET_CHAIN_ID}, received ${chainId}`); } - return readRpcFlowPulseLogSetWithChainId(options, chainId, fetchImpl); + return readRpcFlowPulseLogSetWithChainId(normalizedOptions, chainId, fetchImpl); } diff --git a/services/indexer/test/indexer.test.ts b/services/indexer/test/indexer.test.ts index 17aaba92..919f0b8e 100644 --- a/services/indexer/test/indexer.test.ts +++ b/services/indexer/test/indexer.test.ts @@ -9,6 +9,7 @@ import { parseBaseSepoliaReaderArgs, runBaseSepoliaReader } from "../src/base-se import { indexFlowPulseLogs, indexFlowPulseReceipts } from "../src/indexer.ts"; import { loadIndexerFixtureLogs, loadIndexerFixtureReceipts } from "../src/fixtures.ts"; import { + indexerStateDigest, readBaseCanaryIndexerCheckpoint, readBaseSepoliaIndexerCheckpoint, readIndexerState, @@ -30,6 +31,9 @@ test("indexes FlowPulse fixture logs into canonical observations", () => { assert.equal(state.observations[0].duplicateKind, "unique"); assert.equal(state.pulses.length, 1); assert.equal(state.rootfields.length, 1); + assert.equal(state.dashboardFeed.schema, "flowmemory.indexer.dashboard_feed.v0"); + assert.equal(state.dashboardFeed.dashboardCanonicalObservationCount, 1); + assert.equal(state.dashboardFeed.hasIntegrityWarnings, false); }); test("detects exact duplicate observations", () => { @@ -38,6 +42,8 @@ test("detects exact duplicate observations", () => { assert.equal(state.observations.length, 2); assert.equal(state.observations[1].duplicateKind, "exactDuplicate"); assert.equal(state.duplicates.length, 1); + assert.equal(state.dashboardFeed.duplicateKindCounts.exactDuplicate, 1); + assert.equal(state.dashboardFeed.hasIntegrityWarnings, true); }); test("ingests receipt fixtures and rejects reverted or malformed logs cleanly", () => { @@ -259,8 +265,109 @@ test("runs Base Sepolia reader and persists durable state plus checkpoint", asyn assert.equal(checkpoint.network, "base-sepolia"); assert.equal(checkpoint.chainId, "84532"); assert.equal(checkpoint.lastIndexedBlock, "123456"); + assert.equal(checkpoint.lastScannedBlock, "123456"); + assert.equal(checkpoint.highestObservedBlock, "123456"); + assert.equal(checkpoint.nextFromBlock, "123457"); + assert.equal(checkpoint.emptyRange, false); + assert.equal(checkpoint.stateDigest, indexerStateDigest(result.state)); + assert.equal(checkpoint.dashboardFeed.dashboardCanonicalObservationCount, 1); + assert.equal(checkpoint.safety.productionReady, false); + assert.equal(checkpoint.safety.storesRpcUrl, false); assert.equal(checkpoint.observationCount, 1); assert.equal(checkpoint.statePath, statePath); + assert.equal(readFileSync(statePath, "utf8").includes("example.invalid"), false); + assert.equal(readFileSync(checkpointPath, "utf8").includes("example.invalid"), false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("Base Sepolia reader records RPC and ABI malformed logs in the dashboard feed", async () => { + const [fixtureLog] = loadIndexerFixtureLogs(); + const dir = mkdtempSync(join(tmpdir(), "flowmemory-base-sepolia-malformed-")); + const statePath = join(dir, "state.json"); + const checkpointPath = join(dir, "checkpoint.json"); + const calls: string[] = []; + const fetchImpl = async (_url: string, init?: RequestInit): Promise => { + const body = JSON.parse(String(init?.body)) as { method: string }; + calls.push(body.method); + if (body.method === "eth_chainId") { + return Response.json({ jsonrpc: "2.0", id: 1, result: "0x14a34" }); + } + if (body.method === "eth_getLogs") { + return Response.json({ + jsonrpc: "2.0", + id: 1, + result: [ + { + address: fixtureLog.address, + topics: "not-an-array", + data: fixtureLog.data, + blockNumber: "0x1e240", + blockHash: fixtureLog.blockHash, + transactionHash: fixtureLog.transactionHash, + transactionIndex: "0x0", + logIndex: "0x0", + }, + { + address: fixtureLog.address, + topics: fixtureLog.topics.slice(0, 3), + data: fixtureLog.data, + blockNumber: "0x1e240", + blockHash: fixtureLog.blockHash, + transactionHash: fixtureLog.transactionHash, + transactionIndex: "0x1", + logIndex: "0x1", + }, + { + address: fixtureLog.address, + topics: fixtureLog.topics, + data: fixtureLog.data, + blockNumber: "0x1e240", + blockHash: fixtureLog.blockHash, + transactionHash: fixtureLog.transactionHash, + transactionIndex: "0x2", + logIndex: "0x2", + }, + ], + }); + } + if (body.method === "eth_getTransactionReceipt") { + return Response.json({ jsonrpc: "2.0", id: 1, result: { status: "0x1" } }); + } + return Response.json({ jsonrpc: "2.0", id: 1, error: { code: -32601, message: "not found" } }); + }; + + try { + const result = await runBaseSepoliaReader({ + rpcUrl: "https://example.invalid", + addresses: [fixtureLog.address], + fromBlock: "123456", + toBlock: "123456", + outPath: statePath, + checkpointPath, + generatedAt: "2026-05-13T00:00:00.000Z", + fetchImpl, + }); + const checkpoint = readBaseSepoliaIndexerCheckpoint(checkpointPath); + + assert.deepEqual(calls, [ + "eth_chainId", + "eth_getLogs", + "eth_getTransactionReceipt", + "eth_getTransactionReceipt", + ]); + assert.equal(result.state.observations.length, 1); + assert.equal(result.state.rejectedLogs.length, 2); + assert.deepEqual(result.state.rejectedLogs.map((log) => log.reasonCode), [ + "rpc.log.malformed", + "log.malformed", + ]); + assert.equal(result.state.dashboardFeed.rejectedReasonCounts["rpc.log.malformed"], 1); + assert.equal(result.state.dashboardFeed.rejectedReasonCounts["log.malformed"], 1); + assert.equal(checkpoint.dashboardFeed.hasIntegrityWarnings, true); + assert.equal(checkpoint.dashboardFeed.rejectedLogCount, 2); + assert.equal(checkpoint.emptyRange, false); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -310,6 +417,9 @@ test("runs Base mainnet canary reader and persists empty scans safely", async () assert.equal(checkpoint.observationCount, 0); assert.equal(checkpoint.rejectedLogCount, 0); assert.equal(checkpoint.lastIndexedBlock, "45955500"); + assert.equal(checkpoint.highestObservedBlock, null); + assert.equal(checkpoint.nextFromBlock, "45955501"); + assert.equal(checkpoint.emptyRange, true); } finally { rmSync(dir, { recursive: true, force: true }); } @@ -456,6 +566,97 @@ test("parses Base Sepolia reader CLI args without defaulting to a public RPC", ( () => parseBaseSepoliaReaderArgs(["--address", fixtureLog.address, "--from-block", "1", "--to-block", "2"]), /--rpc-url is required/, ); + assert.throws( + () => parseBaseSepoliaReaderArgs([ + "--rpc-url", + "$BASE_SEPOLIA_RPC_URL", + "--address", + fixtureLog.address, + "--from-block", + "1", + "--to-block", + "2", + ]), + /resolved URL/, + ); + assert.throws( + () => parseBaseSepoliaReaderArgs([ + "--rpc-url", + "https://user:pass@example.invalid", + "--address", + fixtureLog.address, + "--from-block", + "1", + "--to-block", + "2", + ]), + /username\/password credentials/, + ); + assert.throws( + () => parseBaseSepoliaReaderArgs([ + "--rpc-url", + "https://example.invalid", + "--address", + fixtureLog.address, + "--from-block", + "1", + "--to-block", + "10002", + ]), + /refuses broad scans/, + ); +}); + +test("Base Sepolia reader supports safe checkpoint resume after empty ranges", async () => { + const [fixtureLog] = loadIndexerFixtureLogs(); + const dir = mkdtempSync(join(tmpdir(), "flowmemory-base-sepolia-resume-")); + const statePath = join(dir, "state.json"); + const checkpointPath = join(dir, "checkpoint.json"); + const fetchImpl = async (_url: string, init?: RequestInit): Promise => { + const body = JSON.parse(String(init?.body)) as { method: string }; + if (body.method === "eth_chainId") { + return Response.json({ jsonrpc: "2.0", id: 1, result: "0x14a34" }); + } + if (body.method === "eth_getLogs") { + return Response.json({ jsonrpc: "2.0", id: 1, result: [] }); + } + return Response.json({ jsonrpc: "2.0", id: 1, error: { code: -32601, message: "not found" } }); + }; + + try { + const result = await runBaseSepoliaReader({ + rpcUrl: "https://example.invalid", + addresses: [fixtureLog.address], + fromBlock: "123456", + toBlock: "123460", + outPath: statePath, + checkpointPath, + generatedAt: "2026-05-13T00:00:00.000Z", + fetchImpl, + }); + + assert.equal(result.checkpoint.observationCount, 0); + assert.equal(result.checkpoint.lastIndexedBlock, "123460"); + assert.equal(result.checkpoint.nextFromBlock, "123461"); + assert.equal(result.checkpoint.emptyRange, true); + + const parsed = parseBaseSepoliaReaderArgs([ + "--rpc-url", + "https://example.invalid", + "--address", + fixtureLog.address, + "--resume-from-checkpoint", + "--checkpoint-out", + checkpointPath, + "--to-block", + "123470", + ]); + + assert.equal(parsed.fromBlock, "123461"); + assert.equal(parsed.toBlock, "123470"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } }); test("parses guarded Base mainnet canary reader CLI args", () => {